{"homepage":"https://smoothui.dev/","items":[{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["@clack/prompts","picocolors"],"description":"CLI to install SmoothUI components - beautifully designed React components with smooth animations","devDependencies":["@types/node","tsup","tsx","ultracite","vitest"],"files":[{"content":"import { existsSync, readFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { describe, expect, it } from \"vitest\";\n\ninterface PackageJson {\n  bin?: Record<string, string>;\n  files?: string[];\n  name?: string;\n  private?: boolean;\n  publishConfig?: Record<string, unknown>;\n  repository?: { directory?: string };\n  version?: string;\n}\n\ninterface ReleasePleaseConfig {\n  packages: Record<string, { component?: string; \"package-name\"?: string }>;\n}\n\nconst readJson = <Value>(filePath: string): Value =>\n  JSON.parse(\n    readFileSync(path.join(repositoryRoot, filePath), \"utf8\")\n  ) as Value;\n\nconst repositoryRoot = path.resolve(import.meta.dirname, \"../..\");\nconst repositoryPathExists = (filePath: string): boolean =>\n  existsSync(path.join(repositoryRoot, filePath));\n\ndescribe(\"smoothui-cli workspace isolation\", () => {\n  it(\"keeps the repository root private and non-publishable\", () => {\n    const rootPackage = readJson<PackageJson>(\"package.json\");\n\n    expect(rootPackage.private).toBe(true);\n    expect(rootPackage.name).not.toBe(\"smoothui-cli\");\n    expect(rootPackage.bin).toBeUndefined();\n    expect(rootPackage.publishConfig).toBeUndefined();\n    expect(repositoryPathExists(\"scripts/index.ts\")).toBe(false);\n  });\n\n  it(\"owns all publish metadata inside packages/cli\", () => {\n    const cliPackage = readJson<PackageJson>(\"packages/cli/package.json\");\n    const manifest = readJson<Record<string, string>>(\n      \".release-please-manifest.json\"\n    );\n\n    expect(cliPackage).toMatchObject({\n      bin: {\n        smoothui: \"dist/index.js\",\n        \"smoothui-cli\": \"dist/index.js\",\n      },\n      files: [\"dist/index.js\", \"README.md\", \"LICENSE\"],\n      name: \"smoothui-cli\",\n      publishConfig: { access: \"public\", provenance: true },\n      repository: { directory: \"packages/cli\" },\n    });\n    expect(cliPackage.version).toBe(manifest[\"packages/cli\"]);\n    expect(repositoryPathExists(\"packages/cli/scripts/index.ts\")).toBe(true);\n    expect(repositoryPathExists(\"packages/cli/README.md\")).toBe(true);\n    expect(repositoryPathExists(\"packages/cli/LICENSE\")).toBe(true);\n    expect(\n      readFileSync(path.join(repositoryRoot, \"packages/cli/LICENSE\"))\n    ).toEqual(readFileSync(path.join(repositoryRoot, \"LICENSE\")));\n  });\n\n  it(\"uses workspace paths as the release boundaries\", () => {\n    const releaseConfig = readJson<ReleasePleaseConfig>(\n      \"release-please-config.json\"\n    );\n    const manifest = readJson<Record<string, string>>(\n      \".release-please-manifest.json\"\n    );\n    const cliPackage = readJson<PackageJson>(\"packages/cli/package.json\");\n    const libraryPackage = readJson<PackageJson>(\n      \"packages/smoothui/package.json\"\n    );\n\n    expect(releaseConfig.packages).toEqual({\n      \"packages/cli\": {\n        component: \"cli\",\n        \"include-component-in-tag\": true,\n        \"package-name\": \"smoothui-cli\",\n      },\n      \"packages/smoothui\": { \"package-name\": \"smoothui\" },\n    });\n    expect(Object.keys(manifest).sort()).toEqual([\n      \"packages/cli\",\n      \"packages/smoothui\",\n    ]);\n    expect(manifest[\"packages/cli\"]).toBe(cliPackage.version);\n    expect(manifest[\"packages/smoothui\"]).toBe(libraryPackage.version);\n  });\n});\n","path":"package-isolation.test.ts","target":"components/smoothui/cli/package-isolation.test.ts","type":"registry:ui"}],"name":"cli","registryDependencies":[],"title":"Cli","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":[],"devDependencies":[],"files":[{"content":"/**\n * Component metadata types for AI-first developer experience.\n *\n * These types define structured, machine-readable metadata for SmoothUI\n * components and blocks, enabling AI agents to discover, filter, and\n * understand components without reading source code.\n */\n\n// ---------------------------------------------------------------------------\n// Category types\n// ---------------------------------------------------------------------------\n\nexport type ComponentCategory =\n  | \"basic-ui\"\n  | \"button\"\n  | \"text\"\n  | \"ai\"\n  | \"layout\"\n  | \"feedback\"\n  | \"data-display\"\n  | \"navigation\"\n  | \"other\";\n\nexport type AnimationType = \"spring\" | \"tween\" | \"gesture\" | \"scroll\" | \"none\";\n\nexport type Complexity = \"simple\" | \"moderate\" | \"complex\";\n\n// ---------------------------------------------------------------------------\n// Component metadata\n// ---------------------------------------------------------------------------\n\n/**\n * Full metadata for a single SmoothUI component.\n *\n * Combines data from two sources:\n * - `smoothui` field in the component's package.json (authored by hand)\n * - MDX frontmatter and registry info (derived at build time)\n */\nexport interface ComponentMeta {\n  /** Dominant animation technique */\n  animationType: AnimationType;\n  /** Primary UI category */\n  category: ComponentCategory;\n  /** Rough complexity level */\n  complexity: Complexity;\n  /** Hints for combining with other components */\n  compositionHints: readonly string[];\n  /** npm packages required (excluding peer deps) */\n  dependencies: readonly string[];\n  /** 1-2 sentence human-readable description */\n  description: string;\n  /** PascalCase display name, e.g. \"AnimatedTabs\" */\n  displayName: string;\n  /** Full documentation URL */\n  docUrl: string;\n  /** Whether the component respects prefers-reduced-motion */\n  hasReducedMotion: boolean;\n  /** shadcn-style install command */\n  installCommand: string;\n  /** kebab-case identifier, e.g. \"animated-tabs\" */\n  name: string;\n  /** Number of public props */\n  propsCount: number;\n  /** Other SmoothUI components required */\n  registryDependencies: readonly string[];\n  /** Registry JSON URL */\n  registryUrl: string;\n  /** Discoverable tags, e.g. [\"animation\", \"tabs\", \"navigation\"] */\n  tags: readonly string[];\n  /** Natural-language use-case descriptions */\n  useCases: readonly string[];\n}\n\n// ---------------------------------------------------------------------------\n// Block metadata\n// ---------------------------------------------------------------------------\n\nexport type BlockType =\n  | \"hero\"\n  | \"features\"\n  | \"pricing\"\n  | \"testimonials\"\n  | \"cta\"\n  | \"footer\"\n  | \"header\"\n  | \"other\";\n\n/**\n * Full metadata for a pre-built page block (section).\n */\nexport interface BlockMeta {\n  /** Dominant animation technique */\n  animationType: AnimationType;\n  /** Block section type */\n  blockType: BlockType;\n  /** Primary UI category */\n  category: ComponentCategory;\n  /** Rough complexity level */\n  complexity: Complexity;\n  /** SmoothUI components used inside this block */\n  components: readonly string[];\n  /** npm packages required */\n  dependencies: readonly string[];\n  /** 1-2 sentence description */\n  description: string;\n  /** PascalCase display name */\n  displayName: string;\n  /** Full documentation URL */\n  docUrl: string;\n  /** Whether the block respects prefers-reduced-motion */\n  hasReducedMotion: boolean;\n  /** shadcn-style install command */\n  installCommand: string;\n  /** kebab-case identifier */\n  name: string;\n  /** Registry JSON URL */\n  registryUrl: string;\n  /** Discoverable tags */\n  tags: readonly string[];\n  /** Natural-language use-case descriptions */\n  useCases: readonly string[];\n}\n\n// ---------------------------------------------------------------------------\n// API response types\n// ---------------------------------------------------------------------------\n\n/**\n * Paginated list response wrapper used by all list endpoints.\n */\nexport interface PaginatedResponse<T> {\n  data: T[];\n  page: number;\n  pageSize: number;\n  total: number;\n  totalPages: number;\n}\n\n/** GET /api/components */\nexport type ComponentListResponse = PaginatedResponse<ComponentMeta>;\n\n/** GET /api/components/:name */\nexport interface ComponentDetailResponse {\n  component: ComponentMeta;\n  /** Raw source code (optional, included when ?include=source) */\n  source?: string;\n}\n\n/** GET /api/blocks */\nexport type BlockListResponse = PaginatedResponse<BlockMeta>;\n\n/** GET /api/blocks/:name */\nexport interface BlockDetailResponse {\n  block: BlockMeta;\n  source?: string;\n}\n\n/** Standard error envelope returned by API routes */\nexport interface ApiErrorResponse {\n  error: string;\n  status: number;\n}\n\n// ---------------------------------------------------------------------------\n// Query / filter helpers\n// ---------------------------------------------------------------------------\n\n/** Supported filter parameters for component list queries */\nexport interface ComponentQueryParams {\n  animationType?: AnimationType;\n  category?: ComponentCategory;\n  complexity?: Complexity;\n  page?: number;\n  pageSize?: number;\n  search?: string;\n  tag?: string;\n}\n\n/** Supported filter parameters for block list queries */\nexport interface BlockQueryParams {\n  blockType?: BlockType;\n  complexity?: Complexity;\n  page?: number;\n  pageSize?: number;\n  search?: string;\n  tag?: string;\n}\n","path":"component-meta.ts","target":"lib/smoothui-data/component-meta.ts","type":"registry:lib"},{"content":"// ---------------------------------------------------------------------------\n// AI-first metadata types & schema\n// ---------------------------------------------------------------------------\nexport type {\n  AnimationType,\n  ApiErrorResponse,\n  BlockDetailResponse,\n  BlockListResponse,\n  BlockMeta,\n  BlockQueryParams,\n  BlockType,\n  Complexity,\n  ComponentCategory,\n  ComponentDetailResponse,\n  ComponentListResponse,\n  ComponentMeta,\n  ComponentQueryParams,\n  PaginatedResponse,\n} from \"./component-meta\";\n\nexport type {\n  ParseFailure,\n  ParseResult,\n  ParseSuccess,\n  SmoothUIPackageMeta,\n} from \"./smoothui-schema\";\n\nexport { parseSmoothUIMeta } from \"./smoothui-schema\";\n\n// ---------------------------------------------------------------------------\n// People data\n// ---------------------------------------------------------------------------\n\n/**\n * Unified Person interface for all people data\n *\n * This single interface contains all possible fields for people data.\n * Components can use only the fields they need:\n * - Team components: name, role, bio, avatar, location, experience, social, company\n * - Testimonial components: name, role, avatar, stars, content\n * - Mixed components: any combination of fields\n */\nexport interface Person {\n  avatar: string;\n  bio?: string;\n  company?: string;\n  content?: string;\n  experience?: string;\n  location?: string;\n  name: string;\n  role: string;\n  social?: {\n    twitter?: string;\n    linkedin?: string;\n    github?: string;\n    website?: string;\n  };\n  // Testimonial specific fields\n  stars?: number;\n}\n\nexport const peopleData: Person[] = [\n  {\n    avatar:\n      \"https://ik.imagekit.io/16u211libb/avatar-educalvolpz.jpeg?updatedAt=1765524159631\",\n    bio: \"Passionate about building products that make a difference. Leading the vision for innovative user experiences.\",\n    company: \"SmoothUI\",\n    content:\n      \"SmoothUI has revolutionized how we build user interfaces. The animations are buttery smooth and the developer experience is incredible.\",\n    experience: \"8+ years of experience\",\n    location: \"Spain\",\n    name: \"Eduardo Calvo\",\n    role: \"CEO & Founder\",\n    social: {\n      github: \"https://github.com/educlopez\",\n      linkedin: \"https://linkedin.com/in/educlopez\",\n      twitter: \"https://twitter.com/educalvolpz\",\n      website: \"https://educalvolopez.com\",\n    },\n    stars: 5,\n  },\n  {\n    avatar: \"https://ik.imagekit.io/16u211libb/smoothui/avatar.jpg\",\n    bio: \"Creating beautiful and intuitive user experiences that users love. Passionate about design systems and accessibility.\",\n    company: \"Design Studio\",\n    content:\n      \"The design system is incredibly well thought out. Every component feels intentional and polished.\",\n    experience: \"7+ years of experience\",\n    location: \"San Francisco, CA\",\n    name: \"Drew Cano\",\n    role: \"Head of Design\",\n    social: {\n      github: \"https://github.com/educlopez\",\n      linkedin: \"https://linkedin.com/in/educlopez\",\n      twitter: \"https://twitter.com/educalvolpz\",\n      website: \"https://educalvolopez.com\",\n    },\n    stars: 5,\n  },\n  {\n    avatar: \"https://ik.imagekit.io/16u211libb/smoothui/avatar-1.jpg\",\n    bio: \"Building scalable solutions for modern applications. Expert in React, TypeScript, and cloud architecture.\",\n    company: \"TechCorp\",\n    content:\n      \"Best UI library I've used. The TypeScript support is excellent and the components are highly customizable.\",\n    experience: \"10+ years of experience\",\n    location: \"Austin, TX\",\n    name: \"Marcus Johnson\",\n    role: \"Lead Developer\",\n    social: {\n      github: \"https://github.com/educlopez\",\n      linkedin: \"https://linkedin.com/in/educlopez\",\n      twitter: \"https://twitter.com/educalvolpz\",\n      website: \"https://educalvolopez.com\",\n    },\n    stars: 5,\n  },\n  {\n    avatar: \"https://ik.imagekit.io/16u211libb/smoothui/avatar-2.jpg\",\n    bio: \"Driving product strategy and user research to create products that truly solve user problems.\",\n    company: \"ProductCo\",\n    content:\n      \"Our users love the smooth interactions. It's made our product feel premium and professional.\",\n    experience: \"6+ years of experience\",\n    location: \"New York, NY\",\n    name: \"Emily Rodriguez\",\n    role: \"Product Manager\",\n    social: {\n      github: \"https://github.com/educlopez\",\n      linkedin: \"https://linkedin.com/in/educlopez\",\n      twitter: \"https://twitter.com/educalvolpz\",\n      website: \"https://educalvolopez.com\",\n    },\n    stars: 4,\n  },\n  {\n    avatar: \"https://ik.imagekit.io/16u211libb/smoothui/avatar-3.jpg\",\n    bio: \"Full-stack engineer with expertise in distributed systems and team leadership. Building the future of technology.\",\n    company: \"InnovateTech\",\n    content:\n      \"The performance is outstanding. Our bundle size stayed the same while getting beautiful animations.\",\n    experience: \"12+ years of experience\",\n    location: \"Seattle, WA\",\n    name: \"Mollie Hall\",\n    role: \"CTO\",\n    social: {\n      github: \"https://github.com/educlopez\",\n      linkedin: \"https://linkedin.com/in/educlopez\",\n      twitter: \"https://twitter.com/educalvolpz\",\n      website: \"https://educalvolopez.com\",\n    },\n    stars: 5,\n  },\n  {\n    avatar: \"https://ik.imagekit.io/16u211libb/smoothui/avatar-4.jpg\",\n    bio: \"Understanding user behavior and needs to inform design decisions. Passionate about creating inclusive experiences.\",\n    company: \"ResearchLab\",\n    content:\n      \"The accessibility features are top-notch. Every component follows WCAG guidelines perfectly.\",\n    experience: \"5+ years of experience\",\n    location: \"Toronto, Canada\",\n    name: \"Alec Whitten\",\n    role: \"UX Researcher\",\n    social: {\n      github: \"https://github.com/educlopez\",\n      linkedin: \"https://linkedin.com/in/educlopez\",\n      twitter: \"https://twitter.com/educalvolpz\",\n      website: \"https://educalvolopez.com\",\n    },\n    stars: 5,\n  },\n  {\n    avatar: \"https://ik.imagekit.io/16u211libb/smoothui/avatar-5.jpg\",\n    bio: \"Specializing in React, animations, and performance optimization. Creating smooth user experiences.\",\n    company: \"WebStudio\",\n    experience: \"4+ years of experience\",\n    location: \"London, UK\",\n    name: \"Alisa Hester\",\n    role: \"Frontend Engineer\",\n    social: {\n      github: \"https://github.com/educlopez\",\n      linkedin: \"https://linkedin.com/in/educlopez\",\n      twitter: \"https://twitter.com/educalvolpz\",\n      website: \"https://educalvolopez.com\",\n    },\n  },\n  {\n    avatar: \"https://ik.imagekit.io/16u211libb/smoothui/avatar-6.jpg\",\n    bio: \"Building robust APIs and microservices. Expert in Node.js, Python, and cloud infrastructure.\",\n    company: \"BackendPro\",\n    experience: \"6+ years of experience\",\n    location: \"Barcelona, Spain\",\n    name: \"Johnny Bell\",\n    role: \"Backend Engineer\",\n    social: {\n      github: \"https://github.com/educlopez\",\n      linkedin: \"https://linkedin.com/in/educlopez\",\n      twitter: \"https://twitter.com/educalvolpz\",\n      website: \"https://educalvolopez.com\",\n    },\n  },\n  {\n    avatar: \"https://ik.imagekit.io/16u211libb/smoothui/avatar-7.jpg\",\n    bio: \"Automating deployments and ensuring system reliability. Passionate about infrastructure as code.\",\n    company: \"CloudOps\",\n    experience: \"8+ years of experience\",\n    location: \"Berlin, Germany\",\n    name: \"Mia Ward\",\n    role: \"DevOps Engineer\",\n    social: {\n      github: \"https://github.com/educlopez\",\n      linkedin: \"https://linkedin.com/in/educlopez\",\n      twitter: \"https://twitter.com/educalvolpz\",\n      website: \"https://educalvolopez.com\",\n    },\n  },\n  {\n    avatar: \"https://ik.imagekit.io/16u211libb/smoothui/avatar-8.jpg\",\n    bio: \"Driving growth through strategic marketing and community building. Expert in developer relations.\",\n    company: \"GrowthCo\",\n    experience: \"7+ years of experience\",\n    location: \"Singapore\",\n    name: \"Josh Knight\",\n    role: \"Marketing Director\",\n    social: {\n      github: \"https://github.com/educlopez\",\n      linkedin: \"https://linkedin.com/in/educlopez\",\n      twitter: \"https://twitter.com/educalvolpz\",\n      website: \"https://educalvolopez.com\",\n    },\n  },\n\n  {\n    avatar: \"https://ik.imagekit.io/16u211libb/smoothui/avatar-9.jpg\",\n    bio: \"Building beautiful and accessible UI components for React developers.\",\n    company: \"sand/ui\",\n    content: \"SmoothUI is my go-to for fast, beautiful UIs.\",\n    experience: \"5+ years of experience\",\n    location: \"Remote\",\n    name: \"Kelly Myer\",\n    role: \"Creator of Sand/UI\",\n    social: {\n      github: \"https://github.com/educlopez\",\n      linkedin: \"https://linkedin.com/in/educlopez\",\n      twitter: \"https://twitter.com/educalvolpz\",\n      website: \"https://educalvolopez.com\",\n    },\n    stars: 5,\n  },\n];\n\n// Get people who have testimonials (stars and content)\nexport const testimonialsData: Person[] = peopleData.filter(\n  (person) => person.stars && person.content\n);\n\ninterface ImageKitOptions {\n  format?: \"auto\" | \"webp\" | \"jpg\" | \"jpeg\" | \"png\" | \"avif\";\n  height?: number;\n  quality?: number;\n  transformations?: string;\n  width?: number;\n}\n\n/**\n * Build transformation string from options\n */\nfunction buildTransformations(options?: ImageKitOptions): string {\n  if (options?.transformations) {\n    return options.transformations;\n  }\n\n  const parts: string[] = [];\n\n  if (options?.width) {\n    parts.push(`w-${options.width}`);\n  }\n  if (options?.height) {\n    parts.push(`h-${options.height}`);\n  }\n\n  const quality = options?.quality ?? 80;\n  parts.push(`q-${quality}`);\n\n  const format = options?.format ?? \"auto\";\n  parts.push(`f-${format}`);\n\n  return parts.join(\",\");\n}\n\n/**\n * Process full URL and add transformations\n */\nfunction processFullUrl(imagePath: string, transformations: string): string {\n  const url = new URL(imagePath);\n  url.searchParams.delete(\"updatedAt\");\n  const baseUrl = url.origin + url.pathname;\n\n  if (!transformations) {\n    return baseUrl;\n  }\n  return `${baseUrl}?tr=${transformations}`;\n}\n\n/**\n * Build local path URL\n */\nfunction buildLocalPathUrl(imagePath: string, transformations: string): string {\n  const endpoint =\n    process.env.NEXT_PUBLIC_IMAGEKIT_URL_ENDPOINT ||\n    process.env.IMAGEKIT_URL_ENDPOINT ||\n    \"https://ik.imagekit.io/16u211libb\";\n\n  const cleanPath = imagePath.startsWith(\"/\") ? imagePath.slice(1) : imagePath;\n\n  const imageKitPath = cleanPath.startsWith(\"images/\")\n    ? `smoothui/${cleanPath.replace(\"images/\", \"\")}`\n    : `smoothui/${cleanPath}`;\n\n  const baseUrl = `${endpoint}/${imageKitPath}`;\n\n  if (transformations) {\n    return `${baseUrl}?tr=${transformations}`;\n  }\n\n  return baseUrl;\n}\n\n/**\n * Get ImageKit URL for an image with optimized transformations\n * Converts local image paths (/images/...) to ImageKit URLs with bandwidth optimization\n * @param imagePath - Local image path (e.g., \"/images/avatar.jpg\") or already full URL\n * @param options - Optional transformation options\n * @param options.width - Image width in pixels\n * @param options.height - Image height in pixels\n * @param options.quality - Image quality (1-100, default: 80)\n * @param options.format - Image format (auto, webp, jpg, png, etc.)\n * @param options.transformations - Raw transformation string (overrides other options)\n * @returns Full ImageKit URL with optimized transformations\n */\nexport function getImageKitUrl(\n  imagePath: string,\n  options?: ImageKitOptions\n): string {\n  const transformations = buildTransformations(options);\n\n  if (imagePath.startsWith(\"http://\") || imagePath.startsWith(\"https://\")) {\n    return processFullUrl(imagePath, transformations);\n  }\n\n  return buildLocalPathUrl(imagePath, transformations);\n}\n\n/**\n * Helper function to get avatar URL with optimized size and quality\n * @param avatar - Avatar image path or URL\n * @param size - Avatar size in pixels (default: 40, will be doubled for retina)\n * @returns Optimized ImageKit URL for avatar\n */\nexport function getAvatarUrl(avatar: string, size = 40): string {\n  // Double the size for retina displays, use higher quality for avatars\n  const retinaSize = size * 2;\n  return getImageKitUrl(avatar, {\n    format: \"auto\",\n    height: retinaSize,\n    quality: 85, // Higher quality for faces\n    width: retinaSize,\n  });\n}\n\n// Helper function to get team member data (people without testimonials or all people)\nexport function getTeamMembers(\n  count = 4,\n  includeTestimonials = false\n): Person[] {\n  if (includeTestimonials) {\n    return peopleData.slice(0, count);\n  }\n  // Return people who don't have testimonials for team display\n  return peopleData\n    .filter((person) => !(person.stars && person.content))\n    .slice(0, count);\n}\n\n// Helper function to get testimonials data\nexport function getTestimonials(count = 4): Person[] {\n  return testimonialsData.slice(0, count);\n}\n\n// Helper function to get all people data\nexport function getAllPeople(): Person[] {\n  return peopleData;\n}\n\n// Helper function to get people by role\nexport function getPeopleByRole(role: string): Person[] {\n  return peopleData.filter((person) =>\n    person.role.toLowerCase().includes(role.toLowerCase())\n  );\n}\n\n// Helper function to get people with testimonials\nexport function getPeopleWithTestimonials(): Person[] {\n  return testimonialsData;\n}\n","path":"index.ts","target":"lib/smoothui-data/index.ts","type":"registry:lib"},{"content":"/**\n * Schema definition and validation for the `smoothui` field in component\n * package.json files.\n *\n * Each component's package.json may include a `smoothui` object with\n * machine-readable metadata. This module provides the TypeScript type for\n * that field and a runtime validation/parsing function.\n *\n * @example package.json\n * ```json\n * {\n *   \"name\": \"@repo/animated-tabs\",\n *   \"description\": \"Animated tabs component with sliding indicator\",\n *   \"smoothui\": {\n *     \"category\": \"navigation\",\n *     \"tags\": [\"animation\", \"tabs\", \"navigation\"],\n *     \"complexity\": \"moderate\",\n *     \"animationType\": \"spring\",\n *     \"useCases\": [\n *       \"Tab navigation with smooth transitions\",\n *       \"Section switcher with animated indicator\"\n *     ],\n *     \"compositionHints\": [\n *       \"Combine with animated-tooltip for rich tab headers\"\n *     ],\n *     \"hasReducedMotion\": true\n *   }\n * }\n * ```\n */\n\nimport type {\n  AnimationType,\n  Complexity,\n  ComponentCategory,\n} from \"./component-meta\";\n\n// ---------------------------------------------------------------------------\n// Schema type\n// ---------------------------------------------------------------------------\n\n/**\n * Shape of the `smoothui` field inside a component's package.json.\n * Only includes metadata that is authored by hand — derived fields\n * (name, displayName, dependencies, URLs) are computed at build time.\n */\nexport interface SmoothUIPackageMeta {\n  /** Dominant animation technique */\n  animationType: AnimationType;\n  /** Primary UI category */\n  category: ComponentCategory;\n  /** Rough complexity level */\n  complexity: Complexity;\n  /** Hints for combining with other components */\n  compositionHints: string[];\n  /** Whether the component respects prefers-reduced-motion */\n  hasReducedMotion: boolean;\n  /** Discoverable tags */\n  tags: string[];\n  /** Natural-language use-case descriptions */\n  useCases: string[];\n}\n\n// ---------------------------------------------------------------------------\n// Validation constants\n// ---------------------------------------------------------------------------\n\nconst VALID_CATEGORIES: readonly ComponentCategory[] = [\n  \"basic-ui\",\n  \"button\",\n  \"text\",\n  \"ai\",\n  \"layout\",\n  \"feedback\",\n  \"data-display\",\n  \"navigation\",\n  \"other\",\n];\n\nconst VALID_COMPLEXITIES: readonly Complexity[] = [\n  \"simple\",\n  \"moderate\",\n  \"complex\",\n];\n\nconst VALID_ANIMATION_TYPES: readonly AnimationType[] = [\n  \"spring\",\n  \"tween\",\n  \"gesture\",\n  \"scroll\",\n  \"none\",\n];\n\n// ---------------------------------------------------------------------------\n// Validation helpers\n// ---------------------------------------------------------------------------\n\ninterface ValidationError {\n  field: string;\n  message: string;\n}\n\nconst isString = (value: unknown): value is string => typeof value === \"string\";\n\nconst isBoolean = (value: unknown): value is boolean =>\n  typeof value === \"boolean\";\n\nconst isStringArray = (value: unknown): value is string[] =>\n  Array.isArray(value) && value.every(isString);\n\nconst isOneOf = <T extends string>(\n  value: unknown,\n  allowed: readonly T[]\n): value is T =>\n  isString(value) && (allowed as readonly string[]).includes(value);\n\n// ---------------------------------------------------------------------------\n// Parse result\n// ---------------------------------------------------------------------------\n\nexport interface ParseSuccess {\n  data: SmoothUIPackageMeta;\n  success: true;\n}\n\nexport interface ParseFailure {\n  errors: ValidationError[];\n  success: false;\n}\n\nexport type ParseResult = ParseSuccess | ParseFailure;\n\n// ---------------------------------------------------------------------------\n// Parser\n// ---------------------------------------------------------------------------\n\n/**\n * Validate and parse a raw `smoothui` field from a component package.json.\n *\n * Returns a discriminated union so callers can handle errors explicitly.\n *\n * @param raw - The unknown value read from `packageJson.smoothui`\n * @returns A `ParseResult` with either typed data or a list of errors\n */\nexport const parseSmoothUIMeta = (raw: unknown): ParseResult => {\n  const errors: ValidationError[] = [];\n\n  if (raw === null || raw === undefined || typeof raw !== \"object\") {\n    return {\n      errors: [{ field: \"smoothui\", message: \"Must be a non-null object\" }],\n      success: false,\n    };\n  }\n\n  const obj = raw as Record<string, unknown>;\n\n  // category — required\n  if (!isOneOf(obj.category, VALID_CATEGORIES)) {\n    errors.push({\n      field: \"category\",\n      message: `Must be one of: ${VALID_CATEGORIES.join(\", \")}`,\n    });\n  }\n\n  // tags — required string[]\n  if (!isStringArray(obj.tags)) {\n    errors.push({\n      field: \"tags\",\n      message: \"Must be an array of strings\",\n    });\n  }\n\n  // complexity — required\n  if (!isOneOf(obj.complexity, VALID_COMPLEXITIES)) {\n    errors.push({\n      field: \"complexity\",\n      message: `Must be one of: ${VALID_COMPLEXITIES.join(\", \")}`,\n    });\n  }\n\n  // animationType — required\n  if (!isOneOf(obj.animationType, VALID_ANIMATION_TYPES)) {\n    errors.push({\n      field: \"animationType\",\n      message: `Must be one of: ${VALID_ANIMATION_TYPES.join(\", \")}`,\n    });\n  }\n\n  // useCases — required string[]\n  if (!isStringArray(obj.useCases)) {\n    errors.push({\n      field: \"useCases\",\n      message: \"Must be an array of strings\",\n    });\n  }\n\n  // compositionHints — required string[]\n  if (!isStringArray(obj.compositionHints)) {\n    errors.push({\n      field: \"compositionHints\",\n      message: \"Must be an array of strings\",\n    });\n  }\n\n  // hasReducedMotion — required boolean\n  if (!isBoolean(obj.hasReducedMotion)) {\n    errors.push({\n      field: \"hasReducedMotion\",\n      message: \"Must be a boolean\",\n    });\n  }\n\n  if (errors.length > 0) {\n    return { errors, success: false };\n  }\n\n  return {\n    data: {\n      animationType: obj.animationType as AnimationType,\n      category: obj.category as ComponentCategory,\n      complexity: obj.complexity as Complexity,\n      compositionHints: obj.compositionHints as string[],\n      hasReducedMotion: obj.hasReducedMotion as boolean,\n      tags: obj.tags as string[],\n      useCases: obj.useCases as string[],\n    },\n    success: true,\n  };\n};\n","path":"smoothui-schema.ts","target":"lib/smoothui-data/smoothui-schema.ts","type":"registry:lib"}],"name":"data","registryDependencies":[],"title":"Data","type":"registry:lib"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"Centered CTA block with radial glow background","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport SmoothButton from \"@/components/smoothui/smooth-button\";\nimport { motion, useReducedMotion } from \"motion/react\";\n\nconst SPRING = {\n  bounce: 0.1,\n  duration: 0.25,\n  type: \"spring\" as const,\n};\n\nexport function CtaCentered() {\n  const shouldReduceMotion = useReducedMotion();\n\n  return (\n    <section aria-labelledby=\"cta-centered-heading\">\n      <div className=\"relative overflow-hidden bg-muted/50 py-24 md:py-32\">\n        <div\n          aria-hidden=\"true\"\n          className=\"absolute inset-0 bg-[radial-gradient(ellipse_at_center,var(--color-primary)/0.08,transparent_70%)]\"\n        />\n        <motion.div\n          className=\"relative mx-auto max-w-3xl px-6 text-center\"\n          initial={shouldReduceMotion ? { opacity: 1 } : { opacity: 0, y: 24 }}\n          transition={\n            shouldReduceMotion\n              ? { duration: 0 }\n              : { ...SPRING, staggerChildren: 0.08 }\n          }\n          viewport={{ once: true }}\n          whileInView={\n            shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }\n          }\n        >\n          <h2\n            className=\"text-balance font-bold text-3xl tracking-tight md:text-4xl lg:text-5xl\"\n            id=\"cta-centered-heading\"\n          >\n            Ready to build something amazing?\n          </h2>\n          <p className=\"mx-auto mt-4 max-w-xl text-balance text-foreground/70 text-lg\">\n            Start building with beautifully animated components today. Free,\n            open source, and ready for production.\n          </p>\n          <div className=\"mt-8 flex flex-wrap items-center justify-center gap-4\">\n            <SmoothButton size=\"lg\" variant=\"candy\">\n              Get Started\n            </SmoothButton>\n            <SmoothButton size=\"lg\" variant=\"outline\">\n              Learn More\n            </SmoothButton>\n          </div>\n        </motion.div>\n      </div>\n    </section>\n  );\n}\n\nexport default CtaCentered;\n","path":"index.tsx","target":"components/smoothui/cta-1/index.tsx","type":"registry:block"}],"name":"cta-1","registryDependencies":["https://smoothui.dev/r/smooth-button.json"],"title":"Cta 1","type":"registry:block"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"Split layout CTA block with text and illustration","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport SmoothButton from \"@/components/smoothui/smooth-button\";\nimport { getImageKitUrl } from \"@/lib/smoothui-data\";\nimport { motion, useReducedMotion } from \"motion/react\";\n\nconst SPRING = {\n  bounce: 0.1,\n  duration: 0.25,\n  type: \"spring\" as const,\n};\n\nexport function CtaSplit() {\n  const shouldReduceMotion = useReducedMotion();\n\n  return (\n    <section aria-labelledby=\"cta-split-heading\">\n      <div className=\"py-24 md:py-32\">\n        <div className=\"mx-auto grid max-w-6xl items-center gap-12 px-6 md:grid-cols-2\">\n          <motion.div\n            initial={\n              shouldReduceMotion ? { opacity: 1 } : { opacity: 0, x: -24 }\n            }\n            transition={shouldReduceMotion ? { duration: 0 } : SPRING}\n            viewport={{ once: true }}\n            whileInView={\n              shouldReduceMotion ? { opacity: 1 } : { opacity: 1, x: 0 }\n            }\n          >\n            <h2\n              className=\"text-balance font-bold text-3xl tracking-tight md:text-4xl\"\n              id=\"cta-split-heading\"\n            >\n              Ship faster with animated components\n            </h2>\n            <p className=\"mt-4 text-foreground/70 text-lg leading-relaxed\">\n              Stop building UI from scratch. Use production-ready, beautifully\n              animated components that work with your existing design system.\n            </p>\n            <div className=\"mt-8 flex flex-wrap gap-4\">\n              <SmoothButton size=\"lg\" variant=\"candy\">\n                Browse Components\n              </SmoothButton>\n              <SmoothButton size=\"lg\" variant=\"outline\">\n                View on GitHub\n              </SmoothButton>\n            </div>\n          </motion.div>\n\n          <motion.div\n            className=\"relative\"\n            initial={\n              shouldReduceMotion ? { opacity: 1 } : { opacity: 0, x: 24 }\n            }\n            transition={shouldReduceMotion ? { duration: 0 } : SPRING}\n            viewport={{ once: true }}\n            whileInView={\n              shouldReduceMotion ? { opacity: 1 } : { opacity: 1, x: 0 }\n            }\n          >\n            <div className=\"overflow-hidden rounded-2xl border shadow-lg\">\n              <img\n                alt=\"Product preview showing animated UI components\"\n                className=\"h-auto w-full object-cover\"\n                draggable={false}\n                src={getImageKitUrl(\"/images/hero-smoothui.png\", {\n                  format: \"auto\",\n                  quality: 85,\n                  width: 800,\n                })}\n              />\n            </div>\n          </motion.div>\n        </div>\n      </div>\n    </section>\n  );\n}\n\nexport default CtaSplit;\n","path":"index.tsx","target":"components/smoothui/cta-2/index.tsx","type":"registry:block"}],"name":"cta-2","registryDependencies":["https://smoothui.dev/r/smooth-button.json","https://smoothui.dev/r/data.json"],"title":"Cta 2","type":"registry:block"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"Compact banner CTA block","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport SmoothButton from \"@/components/smoothui/smooth-button\";\nimport { motion, useReducedMotion } from \"motion/react\";\n\nconst SPRING = {\n  bounce: 0.1,\n  duration: 0.25,\n  type: \"spring\" as const,\n};\n\nexport function CtaBanner() {\n  const shouldReduceMotion = useReducedMotion();\n\n  return (\n    <section aria-labelledby=\"cta-banner-heading\">\n      <div className=\"px-6 py-12\">\n        <motion.div\n          className=\"mx-auto max-w-4xl overflow-hidden rounded-2xl border bg-gradient-to-r from-primary/5 via-background to-primary/5 p-8 md:p-12\"\n          initial={\n            shouldReduceMotion ? { opacity: 1 } : { opacity: 0, scale: 0.97 }\n          }\n          transition={shouldReduceMotion ? { duration: 0 } : SPRING}\n          viewport={{ once: true }}\n          whileInView={\n            shouldReduceMotion ? { opacity: 1 } : { opacity: 1, scale: 1 }\n          }\n        >\n          <div className=\"flex flex-col items-center justify-between gap-6 md:flex-row\">\n            <div>\n              <h2\n                className=\"font-bold text-xl md:text-2xl\"\n                id=\"cta-banner-heading\"\n              >\n                Start building today\n              </h2>\n              <p className=\"mt-1 text-foreground/70 text-sm md:text-base\">\n                Install any component with a single command.\n              </p>\n            </div>\n            <SmoothButton variant=\"candy\">Get Started →</SmoothButton>\n          </div>\n        </motion.div>\n      </div>\n    </section>\n  );\n}\n\nexport default CtaBanner;\n","path":"index.tsx","target":"components/smoothui/cta-3/index.tsx","type":"registry:block"}],"name":"cta-3","registryDependencies":["https://smoothui.dev/r/smooth-button.json"],"title":"Cta 3","type":"registry:block"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["lucide-react","motion"],"description":"FAQ grid block with categorized tabs and icons","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport {\n  Activity,\n  Clock,\n  DollarSign,\n  Download,\n  Globe,\n  HelpCircle,\n  Lock,\n  MessageCircle,\n  Scale,\n  Settings,\n  ShoppingBag,\n  Undo2,\n  Users,\n  WalletCards,\n  Zap,\n} from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useState } from \"react\";\n\nconst ANIMATION_DURATION = 0.3;\nconst SPRING_STIFFNESS = 500;\nconst SPRING_DAMPING = 30;\nconst HOVER_SCALE = 1.02;\nconst TAP_SCALE = 0.98;\nconst FAQ_STAGGER_DELAY = 0.1;\nconst INITIAL_Y_OFFSET = 20;\nconst EXIT_Y_OFFSET = -20;\n\nconst iconMap = {\n  Activity,\n  Clock,\n  DollarSign,\n  Download,\n  Globe,\n  HelpCircle,\n  Lock,\n  MessageCircle,\n  Scale,\n  Settings,\n  ShoppingBag,\n  Undo2,\n  Users,\n  WalletCards,\n  Zap,\n};\n\ninterface FaqsGridProps {\n  categories?: Array<{\n    name: string;\n    id: string;\n    faqs: Array<{\n      question: string;\n      answer: string;\n      icon: string;\n    }>;\n  }>;\n  description?: string;\n  title?: string;\n}\n\nexport function FaqsGrid({\n  title = \"FAQs\",\n  description = \"Discover quick and comprehensive answers to common questions about our platform, services, and features.\",\n  categories = [\n    {\n      faqs: [\n        {\n          answer:\n            \"You can install Smoothui using npm or yarn. Run `npm install smoothui` or `yarn add smoothui` in your project directory. It's completely free and open source.\",\n          icon: \"Download\",\n          question: \"How do I install Smoothui?\",\n        },\n        {\n          answer:\n            \"Smoothui works with React 16.8+ and supports all modern browsers. No additional dependencies are required beyond React and Tailwind CSS.\",\n          icon: \"Settings\",\n          question: \"What are the system requirements?\",\n        },\n        {\n          answer:\n            \"Yes! Smoothui is completely free and open source. You can start using it immediately without any trial limitations or hidden costs.\",\n          icon: \"Zap\",\n          question: \"Is Smoothui free to use?\",\n        },\n      ],\n      id: \"general\",\n      name: \"General\",\n    },\n    {\n      faqs: [\n        {\n          answer:\n            \"Smoothui includes over 50+ pre-built components including buttons, forms, navigation, modals, animations, and more. New components are added regularly.\",\n          icon: \"HelpCircle\",\n          question: \"How many components are included?\",\n        },\n        {\n          answer:\n            \"Absolutely! All components are fully customizable using CSS variables, Tailwind classes, or custom CSS. You have complete control over the appearance.\",\n          icon: \"Settings\",\n          question: \"Can I customize the styling?\",\n        },\n        {\n          answer:\n            \"Yes, all components follow WCAG guidelines and include proper ARIA attributes for screen readers and keyboard navigation. Accessibility is a top priority.\",\n          icon: \"Users\",\n          question: \"Are the components accessible?\",\n        },\n      ],\n      id: \"components\",\n      name: \"Components\",\n    },\n    {\n      faqs: [\n        {\n          answer:\n            \"You can reach out to our community on Discord, GitHub discussions, or email our support team directly. We're here to help you succeed.\",\n          icon: \"MessageCircle\",\n          question: \"How can I get help?\",\n        },\n        {\n          answer:\n            \"Yes, we offer custom component development and consulting services for enterprise clients. Contact us to discuss your specific needs.\",\n          icon: \"Users\",\n          question: \"Do you offer custom development?\",\n        },\n        {\n          answer:\n            \"We typically respond to support requests within 24 hours during business days. For urgent issues, please mark them as high priority.\",\n          icon: \"Clock\",\n          question: \"What's your response time?\",\n        },\n      ],\n      id: \"support\",\n      name: \"Support\",\n    },\n  ],\n}: FaqsGridProps) {\n  const shouldReduceMotion = useReducedMotion();\n  const [activeTab, setActiveTab] = useState(0);\n\n  return (\n    <section className=\"bg-muted py-16 md:py-24\">\n      <div className=\"mx-auto max-w-5xl px-6\">\n        <div className=\"max-w-lg\">\n          <h2 className=\"font-semibold text-4xl text-foreground\">{title}</h2>\n          <p className=\"mt-4 text-balance text-lg text-muted-foreground\">\n            {description}\n          </p>\n        </div>\n\n        {/* Tabs Navigation */}\n        <div className=\"mt-8 md:mt-12\">\n          <div className=\"flex flex-wrap gap-2 border-border border-b\">\n            {categories.map((category, index) => (\n              <motion.button\n                className={`relative cursor-pointer rounded-t-lg px-4 py-2 font-medium text-sm transition-all duration-200 ${\n                  activeTab === index\n                    ? \"bg-background text-brand\"\n                    : \"border-transparent text-muted-foreground hover:border-border/50 hover:text-foreground\"\n                }`}\n                key={category.id}\n                onClick={() => setActiveTab(index)}\n                type=\"button\"\n                whileHover={shouldReduceMotion ? {} : { scale: HOVER_SCALE }}\n                whileTap={shouldReduceMotion ? {} : { scale: TAP_SCALE }}\n              >\n                {category.name}\n                {activeTab === index && (\n                  <motion.div\n                    className=\"absolute right-0 bottom-0 left-0 h-0.5 rounded-t-full bg-brand\"\n                    initial={false}\n                    layoutId={shouldReduceMotion ? undefined : \"activeTab\"}\n                    transition={\n                      shouldReduceMotion\n                        ? { duration: 0 }\n                        : {\n                            damping: SPRING_DAMPING,\n                            stiffness: SPRING_STIFFNESS,\n                            type: \"spring\" as const,\n                          }\n                    }\n                  />\n                )}\n              </motion.button>\n            ))}\n          </div>\n        </div>\n\n        {/* Tab Content */}\n        <div className=\"mt-8 md:mt-8\">\n          <AnimatePresence mode=\"wait\">\n            <motion.div\n              animate={\n                shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }\n              }\n              exit={\n                shouldReduceMotion\n                  ? { opacity: 0, transition: { duration: 0 } }\n                  : { opacity: 0, y: EXIT_Y_OFFSET }\n              }\n              initial={\n                shouldReduceMotion\n                  ? { opacity: 1 }\n                  : { opacity: 0, y: INITIAL_Y_OFFSET }\n              }\n              key={activeTab}\n              transition={\n                shouldReduceMotion\n                  ? { duration: 0 }\n                  : { duration: ANIMATION_DURATION, ease: \"easeInOut\" }\n              }\n            >\n              <div className=\"space-y-6\">\n                <dl className=\"grid gap-12 sm:grid-cols-2 lg:grid-cols-3\">\n                  {categories[activeTab].faqs.map((faq, faqIndex) => {\n                    const IconComponent =\n                      iconMap[faq.icon as keyof typeof iconMap] || HelpCircle;\n\n                    return (\n                      <motion.div\n                        animate={\n                          shouldReduceMotion\n                            ? { opacity: 1 }\n                            : { opacity: 1, y: 0 }\n                        }\n                        className=\"space-y-3\"\n                        initial={\n                          shouldReduceMotion\n                            ? { opacity: 1 }\n                            : { opacity: 0, y: INITIAL_Y_OFFSET }\n                        }\n                        key={`${categories[activeTab].id}-${faqIndex}`}\n                        transition={\n                          shouldReduceMotion\n                            ? { duration: 0 }\n                            : {\n                                delay: faqIndex * FAQ_STAGGER_DELAY,\n                                duration: 0.4,\n                              }\n                        }\n                      >\n                        <dt className=\"space-y-3 font-semibold text-foreground\">\n                          <div className=\"flex size-8 items-center justify-center rounded-md border bg-card *:m-auto *:size-4\">\n                            <IconComponent className=\"h-4 w-4\" />\n                          </div>\n                          {faq.question}\n                        </dt>\n                        <dd className=\"text-muted-foreground\">{faq.answer}</dd>\n                      </motion.div>\n                    );\n                  })}\n                </dl>\n              </div>\n            </motion.div>\n          </AnimatePresence>\n        </div>\n      </div>\n    </section>\n  );\n}\n\nexport default FaqsGrid;\n","path":"index.tsx","target":"components/smoothui/faq-1/index.tsx","type":"registry:block"}],"name":"faq-1","registryDependencies":["https://smoothui.dev/r/tokens.json"],"title":"Faq 1","type":"registry:block"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"FAQ accordion block with expandable questions","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useState } from \"react\";\n\nconst ANIMATION_DURATION = 0.6;\nconst STAGGER_DELAY = 0.1;\nconst INITIAL_Y_OFFSET = 20;\nconst HOVER_SCALE = 1.01;\nconst TAP_SCALE = 0.99;\nconst ROTATION_OPEN = 180;\nconst ROTATION_CLOSED = 0;\nconst CONTENT_DELAY = 0.1;\nconst INITIAL_CONTENT_Y = -10;\n\ninterface FaqsAccordionProps {\n  description?: string;\n  faqs?: Array<{\n    question: string;\n    answer: string;\n  }>;\n  title?: string;\n}\n\nexport function FaqsAccordion({\n  title = \"Frequently Asked Questions\",\n  description = \"Find answers to common questions about our product and services\",\n  faqs = [\n    {\n      answer:\n        \"Smoothui is a modern UI component library that provides beautiful, animated components for building stunning user interfaces. It includes pre-built components with smooth animations and customizable themes.\",\n      question: \"What is Smoothui?\",\n    },\n    {\n      answer:\n        \"Getting started is easy! Simply install the package via npm or yarn, import the components you need, and start building. We provide comprehensive documentation and examples to help you get up and running quickly.\",\n      question: \"How do I get started?\",\n    },\n    {\n      answer:\n        \"Yes! Smoothui is completely free and open source. You can use it in both personal and commercial projects without any restrictions. We believe in making beautiful UI components accessible to everyone.\",\n      question: \"Is it free to use?\",\n    },\n    {\n      answer:\n        \"Absolutely! All components are fully customizable. You can modify colors, spacing, animations, and more using CSS variables or by extending the component classes. We also provide a theming system for easy customization.\",\n      question: \"Can I customize the components?\",\n    },\n    {\n      answer:\n        \"Currently, Smoothui supports React and Next.js. We're working on expanding support to other popular frameworks like Vue, Svelte, and Angular in the coming months.\",\n      question: \"What frameworks are supported?\",\n    },\n    {\n      answer:\n        \"We release updates regularly, typically every 2-3 weeks. This includes new components, bug fixes, performance improvements, and new features. You can follow our changelog to stay updated on the latest releases.\",\n      question: \"How often do you release updates?\",\n    },\n  ],\n}: FaqsAccordionProps) {\n  const shouldReduceMotion = useReducedMotion();\n  const [openIndex, setOpenIndex] = useState<number | null>(0);\n\n  const toggleAccordion = (index: number) => {\n    setOpenIndex(openIndex === index ? null : index);\n  };\n\n  return (\n    <section className=\"py-20\">\n      <div className=\"mx-auto max-w-4xl px-6\">\n        <motion.div\n          animate={shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }}\n          className=\"mb-16 text-center\"\n          initial={\n            shouldReduceMotion\n              ? { opacity: 1 }\n              : { opacity: 0, y: INITIAL_Y_OFFSET }\n          }\n          transition={\n            shouldReduceMotion\n              ? { duration: 0 }\n              : { duration: ANIMATION_DURATION }\n          }\n          viewport={{ once: true }}\n          whileInView={\n            shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }\n          }\n        >\n          <h2 className=\"mb-4 font-bold text-3xl text-foreground lg:text-4xl\">\n            {title}\n          </h2>\n          <p className=\"mx-auto max-w-2xl text-foreground/70 text-lg\">\n            {description}\n          </p>\n        </motion.div>\n\n        <div className=\"space-y-4\">\n          {faqs.map((faq, index) => (\n            <motion.div\n              animate={\n                shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }\n              }\n              className=\"group overflow-hidden rounded-2xl border border-border bg-background transition-all hover:border-brand hover:shadow-lg\"\n              initial={\n                shouldReduceMotion\n                  ? { opacity: 1 }\n                  : { opacity: 0, y: INITIAL_Y_OFFSET }\n              }\n              key={faq.question}\n              transition={\n                shouldReduceMotion\n                  ? { duration: 0 }\n                  : {\n                      delay: index * STAGGER_DELAY,\n                      duration: ANIMATION_DURATION,\n                    }\n              }\n              viewport={{ once: true }}\n              whileInView={\n                shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }\n              }\n            >\n              <motion.button\n                className=\"flex w-full cursor-pointer items-center justify-between p-6 text-left transition-colors hover:bg-background/50\"\n                onClick={() => toggleAccordion(index)}\n                type=\"button\"\n                whileHover={shouldReduceMotion ? {} : { scale: HOVER_SCALE }}\n                whileTap={shouldReduceMotion ? {} : { scale: TAP_SCALE }}\n              >\n                <h3 className=\"pr-4 font-semibold text-foreground text-lg\">\n                  {faq.question}\n                </h3>\n                <motion.div\n                  animate={\n                    shouldReduceMotion\n                      ? {\n                          rotate:\n                            openIndex === index\n                              ? ROTATION_OPEN\n                              : ROTATION_CLOSED,\n                        }\n                      : {\n                          rotate:\n                            openIndex === index\n                              ? ROTATION_OPEN\n                              : ROTATION_CLOSED,\n                        }\n                  }\n                  className=\"flex-shrink-0\"\n                  transition={\n                    shouldReduceMotion\n                      ? { duration: 0 }\n                      : { duration: ANIMATION_DURATION, ease: \"easeInOut\" }\n                  }\n                >\n                  <svg\n                    aria-hidden=\"true\"\n                    className=\"h-5 w-5 text-foreground/60\"\n                    fill=\"none\"\n                    stroke=\"currentColor\"\n                    viewBox=\"0 0 24 24\"\n                  >\n                    <path\n                      d=\"M19 9l-7 7-7-7\"\n                      strokeLinecap=\"round\"\n                      strokeLinejoin=\"round\"\n                      strokeWidth={2}\n                    />\n                  </svg>\n                </motion.div>\n              </motion.button>\n\n              <AnimatePresence>\n                {openIndex === index && (\n                  <motion.div\n                    animate={{ height: \"auto\", opacity: 1 }}\n                    className=\"overflow-hidden\"\n                    exit={\n                      shouldReduceMotion\n                        ? { opacity: 0, transition: { duration: 0 } }\n                        : { height: 0, opacity: 0 }\n                    }\n                    initial={\n                      shouldReduceMotion\n                        ? { opacity: 1 }\n                        : { height: 0, opacity: 0 }\n                    }\n                    transition={\n                      shouldReduceMotion\n                        ? { duration: 0 }\n                        : { duration: ANIMATION_DURATION, ease: \"easeInOut\" }\n                    }\n                  >\n                    <motion.div\n                      animate={{ y: 0 }}\n                      className=\"px-6 pb-6\"\n                      exit={\n                        shouldReduceMotion\n                          ? { transition: { duration: 0 } }\n                          : { y: INITIAL_CONTENT_Y }\n                      }\n                      initial={\n                        shouldReduceMotion ? { y: 0 } : { y: INITIAL_CONTENT_Y }\n                      }\n                      transition={\n                        shouldReduceMotion\n                          ? { duration: 0 }\n                          : {\n                              delay: CONTENT_DELAY,\n                              duration: ANIMATION_DURATION,\n                            }\n                      }\n                    >\n                      <p className=\"text-foreground/70 leading-relaxed\">\n                        {faq.answer}\n                      </p>\n                    </motion.div>\n                  </motion.div>\n                )}\n              </AnimatePresence>\n            </motion.div>\n          ))}\n        </div>\n      </div>\n    </section>\n  );\n}\n\nexport default FaqsAccordion;\n","path":"index.tsx","target":"components/smoothui/faq-2/index.tsx","type":"registry:block"}],"name":"faq-2","registryDependencies":["https://smoothui.dev/r/tokens.json"],"title":"Faq 2","type":"registry:block"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["lucide-react","motion"],"description":"Searchable FAQ section with filter animations","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { ChevronDown, Search } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useMemo, useState } from \"react\";\n\nexport interface FaqSearchableProps {\n  description?: string;\n  faqs?: Array<{\n    question: string;\n    answer: string;\n  }>;\n  noResultsText?: string;\n  searchPlaceholder?: string;\n  title?: string;\n}\n\nconst defaultFaqs = [\n  {\n    answer:\n      \"Getting started is easy! Simply install the package via npm or pnpm, import the components you need, and start building. We provide comprehensive documentation and examples to help you get up and running quickly.\",\n    question: \"How do I get started with SmoothUI?\",\n  },\n  {\n    answer:\n      \"Yes! SmoothUI is completely free and open source. You can use it in both personal and commercial projects without any restrictions. We believe in making beautiful UI components accessible to everyone.\",\n    question: \"Is SmoothUI free to use?\",\n  },\n  {\n    answer:\n      \"SmoothUI requires React 18 or later and works with Next.js 13+. You'll also need Node.js 18+ and a modern browser that supports CSS animations and transforms.\",\n    question: \"What are the system requirements?\",\n  },\n  {\n    answer:\n      \"Absolutely! All animations are fully customizable. You can modify timing, easing, and effects using Motion (Framer Motion) props. We also respect the prefers-reduced-motion setting for accessibility.\",\n    question: \"Can I customize the animations?\",\n  },\n  {\n    answer:\n      \"You can report bugs by opening an issue on our GitHub repository. Please include a detailed description, steps to reproduce, and your environment details. We actively monitor and respond to issues.\",\n    question: \"How do I report bugs or issues?\",\n  },\n  {\n    answer:\n      \"Yes, we offer enterprise support packages that include priority bug fixes, dedicated support channels, custom component development, and SLA guarantees. Contact us for more information.\",\n    question: \"Is there enterprise support available?\",\n  },\n];\n\nexport function FaqSearchable({\n  title = \"Frequently Asked Questions\",\n  description = \"Search through our FAQ to find answers to your questions\",\n  searchPlaceholder = \"Search questions...\",\n  noResultsText = \"No matching questions found. Try a different search term.\",\n  faqs = defaultFaqs,\n}: FaqSearchableProps) {\n  const [searchQuery, setSearchQuery] = useState(\"\");\n  const [openIndex, setOpenIndex] = useState<number | null>(null);\n  const shouldReduceMotion = useReducedMotion();\n\n  const filteredFaqs = useMemo(() => {\n    if (!searchQuery.trim()) {\n      return faqs;\n    }\n    const query = searchQuery.toLowerCase();\n    return faqs.filter(\n      (faq) =>\n        faq.question.toLowerCase().includes(query) ||\n        faq.answer.toLowerCase().includes(query)\n    );\n  }, [faqs, searchQuery]);\n\n  const toggleAccordion = (index: number) => {\n    setOpenIndex(openIndex === index ? null : index);\n  };\n\n  const springTransition = shouldReduceMotion\n    ? { duration: 0 }\n    : { bounce: 0.05, duration: 0.25, type: \"spring\" as const };\n\n  const contentTransition = shouldReduceMotion\n    ? { duration: 0 }\n    : { bounce: 0, duration: 0.25, type: \"spring\" as const };\n\n  return (\n    <section className=\"py-20\">\n      <div className=\"mx-auto max-w-4xl px-6\">\n        <motion.div\n          animate={shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }}\n          className=\"mb-12 text-center\"\n          initial={shouldReduceMotion ? { opacity: 1 } : { opacity: 0, y: 20 }}\n          transition={springTransition}\n        >\n          <h2 className=\"mb-4 font-bold text-3xl text-foreground lg:text-4xl\">\n            {title}\n          </h2>\n          <p className=\"mx-auto max-w-2xl text-foreground/70 text-lg\">\n            {description}\n          </p>\n        </motion.div>\n\n        <motion.div\n          animate={shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }}\n          className=\"relative mb-8\"\n          initial={shouldReduceMotion ? { opacity: 1 } : { opacity: 0, y: 20 }}\n          transition={{\n            ...springTransition,\n            delay: shouldReduceMotion ? 0 : 0.1,\n          }}\n        >\n          <Search className=\"absolute top-1/2 left-4 h-5 w-5 -translate-y-1/2 text-foreground/40\" />\n          <input\n            aria-label=\"Search frequently asked questions\"\n            className=\"w-full rounded-xl border border-border bg-background py-4 pr-4 pl-12 text-foreground transition-colors placeholder:text-foreground/40 focus:border-brand focus:outline-none focus:ring-2 focus:ring-brand/20\"\n            onChange={(e) => {\n              setSearchQuery(e.target.value);\n              setOpenIndex(null);\n            }}\n            placeholder={searchPlaceholder}\n            type=\"text\"\n            value={searchQuery}\n          />\n        </motion.div>\n\n        <div className=\"space-y-4\">\n          <AnimatePresence mode=\"popLayout\">\n            {filteredFaqs.length === 0 ? (\n              <motion.div\n                animate={\n                  shouldReduceMotion ? { opacity: 1 } : { opacity: 1, scale: 1 }\n                }\n                className=\"rounded-xl border border-border bg-background/50 py-12 text-center\"\n                exit={\n                  shouldReduceMotion\n                    ? { opacity: 0, transition: { duration: 0 } }\n                    : { opacity: 0, scale: 0.95 }\n                }\n                initial={\n                  shouldReduceMotion\n                    ? { opacity: 1 }\n                    : { opacity: 0, scale: 0.95 }\n                }\n                key=\"no-results\"\n                transition={springTransition}\n              >\n                <p className=\"text-foreground/60\">{noResultsText}</p>\n              </motion.div>\n            ) : (\n              filteredFaqs.map((faq, index) => {\n                const originalIndex = faqs.indexOf(faq);\n                const isOpen = openIndex === originalIndex;\n\n                return (\n                  <motion.div\n                    animate={\n                      shouldReduceMotion\n                        ? { opacity: 1 }\n                        : { opacity: 1, scale: 1, y: 0 }\n                    }\n                    className=\"group overflow-hidden rounded-xl border border-border bg-background transition-colors hover:border-brand\"\n                    exit={\n                      shouldReduceMotion\n                        ? { opacity: 0, transition: { duration: 0 } }\n                        : { opacity: 0, scale: 0.95, y: -10 }\n                    }\n                    initial={\n                      shouldReduceMotion\n                        ? { opacity: 1 }\n                        : { opacity: 0, scale: 0.95, y: 20 }\n                    }\n                    key={faq.question}\n                    layout={!shouldReduceMotion}\n                    transition={{\n                      ...springTransition,\n                      delay: shouldReduceMotion ? 0 : index * 0.05,\n                    }}\n                  >\n                    <button\n                      aria-expanded={isOpen}\n                      className=\"flex w-full cursor-pointer items-center justify-between p-5 text-left transition-colors hover:bg-background/50\"\n                      onClick={() => toggleAccordion(originalIndex)}\n                      type=\"button\"\n                    >\n                      <h3 className=\"pr-4 font-medium text-foreground\">\n                        {faq.question}\n                      </h3>\n                      <motion.div\n                        animate={{\n                          rotate: isOpen ? 180 : 0,\n                        }}\n                        className=\"flex-shrink-0\"\n                        transition={springTransition}\n                      >\n                        <ChevronDown\n                          aria-hidden=\"true\"\n                          className=\"h-5 w-5 text-foreground/60\"\n                        />\n                      </motion.div>\n                    </button>\n\n                    <AnimatePresence>\n                      {isOpen && (\n                        <motion.div\n                          animate={\n                            shouldReduceMotion\n                              ? { height: \"auto\", opacity: 1 }\n                              : { height: \"auto\", opacity: 1 }\n                          }\n                          className=\"overflow-hidden\"\n                          exit={\n                            shouldReduceMotion\n                              ? {\n                                  height: 0,\n                                  opacity: 0,\n                                  transition: { duration: 0 },\n                                }\n                              : { height: 0, opacity: 0 }\n                          }\n                          initial={\n                            shouldReduceMotion\n                              ? { height: \"auto\", opacity: 1 }\n                              : { height: 0, opacity: 0 }\n                          }\n                          transition={contentTransition}\n                        >\n                          <div className=\"px-5 pb-5\">\n                            <p className=\"text-foreground/70 leading-relaxed\">\n                              {faq.answer}\n                            </p>\n                          </div>\n                        </motion.div>\n                      )}\n                    </AnimatePresence>\n                  </motion.div>\n                );\n              })\n            )}\n          </AnimatePresence>\n        </div>\n      </div>\n    </section>\n  );\n}\n\nexport default FaqSearchable;\n","path":"index.tsx","target":"components/smoothui/faq-3/index.tsx","type":"registry:block"}],"name":"faq-3","registryDependencies":["https://smoothui.dev/r/tokens.json"],"title":"Faq 3","type":"registry:block"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["lucide-react","motion"],"description":"Categorized FAQ section with tab navigation","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { ChevronDown } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useState } from \"react\";\n\nexport interface FaqCategorizedProps {\n  categories?: Array<{\n    name: string;\n    faqs: Array<{\n      question: string;\n      answer: string;\n    }>;\n  }>;\n  description?: string;\n  title?: string;\n}\n\nconst defaultCategories = [\n  {\n    faqs: [\n      {\n        answer:\n          \"Setting up SmoothUI is straightforward. Install the package using npm or pnpm, then import the components you need. Our CLI tool can also help scaffold components directly into your project with the right dependencies.\",\n        question: \"How do I set up SmoothUI in my project?\",\n      },\n      {\n        answer:\n          \"SmoothUI requires React 18 or later, Next.js 13+, and Node.js 18+. You'll also need Tailwind CSS configured in your project. All modern browsers are supported.\",\n        question: \"What are the minimum requirements?\",\n      },\n      {\n        answer:\n          \"Use our CLI command `npx shadcn@latest add @smoothui/component-name` to add any component. This will install the component with all its dependencies and place it in your components directory.\",\n        question: \"How do I add my first component?\",\n      },\n    ],\n    name: \"Getting Started\",\n  },\n  {\n    faqs: [\n      {\n        answer:\n          \"Yes, SmoothUI is completely free and open source under the MIT license. You can use it in personal and commercial projects without any cost or attribution requirements.\",\n        question: \"Is SmoothUI free to use?\",\n      },\n      {\n        answer:\n          \"Since SmoothUI is free, there are no purchases to refund. For any premium services or support packages we may offer in the future, our refund policy will be clearly stated.\",\n        question: \"Do you offer refunds?\",\n      },\n      {\n        answer:\n          \"Currently, all components are free. If we introduce premium features, we'll support major credit cards, PayPal, and other popular payment methods through our secure payment processor.\",\n        question: \"What payment methods do you accept?\",\n      },\n    ],\n    name: \"Billing\",\n  },\n  {\n    faqs: [\n      {\n        answer:\n          \"SmoothUI supports all modern browsers including Chrome, Firefox, Safari, and Edge. We test across the latest versions and one version back for each browser to ensure compatibility.\",\n        question: \"Which browsers are supported?\",\n      },\n      {\n        answer:\n          \"Components are optimized for performance with tree-shaking support, minimal bundle size, and hardware-accelerated animations. We only animate transform and opacity properties to ensure 60fps animations.\",\n        question: \"How does SmoothUI affect performance?\",\n      },\n      {\n        answer:\n          \"Absolutely! SmoothUI is built with TypeScript and provides full type definitions for all components. You get autocomplete, type checking, and inline documentation in supported editors.\",\n        question: \"Is TypeScript supported?\",\n      },\n      {\n        answer:\n          \"Yes, all components use Tailwind CSS and CSS variables for styling. You can override styles using className props, customize the design tokens, or modify the component source directly.\",\n        question: \"Can I customize component styles?\",\n      },\n    ],\n    name: \"Technical\",\n  },\n];\n\nexport function FaqCategorized({\n  title = \"Frequently Asked Questions\",\n  description = \"Find answers organized by topic\",\n  categories = defaultCategories,\n}: FaqCategorizedProps) {\n  const [activeCategory, setActiveCategory] = useState(0);\n  const [openIndex, setOpenIndex] = useState<number | null>(null);\n  const shouldReduceMotion = useReducedMotion();\n\n  const springTransition = shouldReduceMotion\n    ? { duration: 0 }\n    : { bounce: 0.05, duration: 0.25, type: \"spring\" as const };\n\n  const contentTransition = shouldReduceMotion\n    ? { duration: 0 }\n    : { bounce: 0, duration: 0.25, type: \"spring\" as const };\n\n  const handleCategoryChange = (index: number) => {\n    setActiveCategory(index);\n    setOpenIndex(null);\n  };\n\n  const toggleAccordion = (index: number) => {\n    setOpenIndex(openIndex === index ? null : index);\n  };\n\n  return (\n    <section className=\"py-20\">\n      <div className=\"mx-auto max-w-4xl px-6\">\n        <motion.div\n          animate={shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }}\n          className=\"mb-12 text-center\"\n          initial={shouldReduceMotion ? { opacity: 1 } : { opacity: 0, y: 20 }}\n          transition={springTransition}\n        >\n          <h2 className=\"mb-4 font-bold text-3xl text-foreground lg:text-4xl\">\n            {title}\n          </h2>\n          <p className=\"mx-auto max-w-2xl text-foreground/70 text-lg\">\n            {description}\n          </p>\n        </motion.div>\n\n        <motion.div\n          animate={shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }}\n          className=\"mb-8\"\n          initial={shouldReduceMotion ? { opacity: 1 } : { opacity: 0, y: 20 }}\n          transition={{\n            ...springTransition,\n            delay: shouldReduceMotion ? 0 : 0.1,\n          }}\n        >\n          <div\n            className=\"flex flex-wrap justify-center gap-2 border-border border-b\"\n            role=\"tablist\"\n          >\n            {categories.map((category, index) => (\n              <button\n                aria-selected={activeCategory === index}\n                className={`relative px-4 py-3 font-medium text-sm transition-colors ${\n                  activeCategory === index\n                    ? \"text-brand\"\n                    : \"text-foreground/60 hover:text-foreground\"\n                }`}\n                key={category.name}\n                onClick={() => handleCategoryChange(index)}\n                role=\"tab\"\n                type=\"button\"\n              >\n                {category.name}\n                {activeCategory === index && (\n                  <motion.div\n                    className=\"absolute right-0 bottom-0 left-0 h-0.5 bg-brand\"\n                    layoutId=\"categoryIndicator\"\n                    transition={springTransition}\n                  />\n                )}\n              </button>\n            ))}\n          </div>\n        </motion.div>\n\n        <AnimatePresence mode=\"wait\">\n          <motion.div\n            animate={shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }}\n            exit={\n              shouldReduceMotion\n                ? { opacity: 0, transition: { duration: 0 } }\n                : { opacity: 0, y: -10 }\n            }\n            initial={\n              shouldReduceMotion ? { opacity: 1 } : { opacity: 0, y: 10 }\n            }\n            key={activeCategory}\n            transition={springTransition}\n          >\n            <div className=\"space-y-4\">\n              {categories[activeCategory].faqs.map((faq, index) => {\n                const isOpen = openIndex === index;\n\n                return (\n                  <motion.div\n                    animate={\n                      shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }\n                    }\n                    className=\"overflow-hidden rounded-xl border border-border bg-background transition-colors hover:border-brand\"\n                    initial={\n                      shouldReduceMotion\n                        ? { opacity: 1 }\n                        : { opacity: 0, y: 20 }\n                    }\n                    key={faq.question}\n                    transition={{\n                      ...springTransition,\n                      delay: shouldReduceMotion ? 0 : index * 0.05,\n                    }}\n                  >\n                    <button\n                      aria-expanded={isOpen}\n                      className=\"flex w-full cursor-pointer items-center justify-between p-5 text-left transition-colors hover:bg-background/50\"\n                      onClick={() => toggleAccordion(index)}\n                      type=\"button\"\n                    >\n                      <h3 className=\"pr-4 font-medium text-foreground\">\n                        {faq.question}\n                      </h3>\n                      <motion.div\n                        animate={{ rotate: isOpen ? 180 : 0 }}\n                        className=\"flex-shrink-0\"\n                        transition={springTransition}\n                      >\n                        <ChevronDown\n                          aria-hidden=\"true\"\n                          className=\"h-5 w-5 text-foreground/60\"\n                        />\n                      </motion.div>\n                    </button>\n\n                    <AnimatePresence>\n                      {isOpen && (\n                        <motion.div\n                          animate={\n                            shouldReduceMotion\n                              ? { height: \"auto\", opacity: 1 }\n                              : { height: \"auto\", opacity: 1 }\n                          }\n                          className=\"overflow-hidden\"\n                          exit={\n                            shouldReduceMotion\n                              ? {\n                                  height: 0,\n                                  opacity: 0,\n                                  transition: { duration: 0 },\n                                }\n                              : { height: 0, opacity: 0 }\n                          }\n                          initial={\n                            shouldReduceMotion\n                              ? { height: \"auto\", opacity: 1 }\n                              : { height: 0, opacity: 0 }\n                          }\n                          transition={contentTransition}\n                        >\n                          <div className=\"px-5 pb-5\">\n                            <p className=\"text-foreground/70 leading-relaxed\">\n                              {faq.answer}\n                            </p>\n                          </div>\n                        </motion.div>\n                      )}\n                    </AnimatePresence>\n                  </motion.div>\n                );\n              })}\n            </div>\n          </motion.div>\n        </AnimatePresence>\n      </div>\n    </section>\n  );\n}\n\nexport default FaqCategorized;\n","path":"index.tsx","target":"components/smoothui/faq-4/index.tsx","type":"registry:block"}],"name":"faq-4","registryDependencies":["https://smoothui.dev/r/tokens.json"],"title":"Faq 4","type":"registry:block"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"Feature grid block with animated cards","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { useEffect, useState } from \"react\";\n\nconst SPRING = {\n  bounce: 0.1,\n  duration: 0.25,\n  type: \"spring\" as const,\n};\n\nconst features = [\n  {\n    description:\n      \"Optimized for performance with minimal bundle size and efficient rendering.\",\n    icon: (\n      <svg\n        aria-hidden=\"true\"\n        className=\"h-6 w-6\"\n        fill=\"none\"\n        stroke=\"currentColor\"\n        strokeWidth={1.5}\n        viewBox=\"0 0 24 24\"\n      >\n        <path\n          d=\"m3.75 13.5 10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n        />\n      </svg>\n    ),\n    title: \"Lightning Fast\",\n  },\n  {\n    description:\n      \"Built with ARIA attributes, keyboard navigation, and reduced motion support.\",\n    icon: (\n      <svg\n        aria-hidden=\"true\"\n        className=\"h-6 w-6\"\n        fill=\"none\"\n        stroke=\"currentColor\"\n        strokeWidth={1.5}\n        viewBox=\"0 0 24 24\"\n      >\n        <path\n          d=\"M12 18v-5.25m0 0a6.01 6.01 0 001.5-.189m-1.5.189a6.01 6.01 0 01-1.5-.189m3.75 7.478a12.06 12.06 0 01-4.5 0m3.75 2.383a14.406 14.406 0 01-3 0M14.25 18v-.192c0-.983.658-1.823 1.508-2.316a7.5 7.5 0 10-7.517 0c.85.493 1.509 1.333 1.509 2.316V18\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n        />\n      </svg>\n    ),\n    title: \"Accessible\",\n  },\n  {\n    description:\n      \"Fully customizable with Tailwind CSS classes and design tokens.\",\n    icon: (\n      <svg\n        aria-hidden=\"true\"\n        className=\"h-6 w-6\"\n        fill=\"none\"\n        stroke=\"currentColor\"\n        strokeWidth={1.5}\n        viewBox=\"0 0 24 24\"\n      >\n        <path\n          d=\"M9.53 16.122a3 3 0 00-5.78 1.128 2.25 2.25 0 01-2.4 2.245 4.5 4.5 0 008.4-2.245c0-.399-.078-.78-.22-1.128zm0 0a15.998 15.998 0 003.388-1.62m-5.043-.025a15.994 15.994 0 011.622-3.395m3.42 3.42a15.995 15.995 0 004.764-4.648l3.876-5.814a1.151 1.151 0 00-1.597-1.597L14.146 6.32a15.996 15.996 0 00-4.649 4.763m3.42 3.42a6.776 6.776 0 00-3.42-3.42\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n        />\n      </svg>\n    ),\n    title: \"Customizable\",\n  },\n  {\n    description:\n      \"Full TypeScript support with exported types for all component props.\",\n    icon: (\n      <svg\n        aria-hidden=\"true\"\n        className=\"h-6 w-6\"\n        fill=\"none\"\n        stroke=\"currentColor\"\n        strokeWidth={1.5}\n        viewBox=\"0 0 24 24\"\n      >\n        <path\n          d=\"M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n        />\n      </svg>\n    ),\n    title: \"TypeScript First\",\n  },\n  {\n    description:\n      \"Smooth spring animations powered by Motion with configurable easing.\",\n    icon: (\n      <svg\n        aria-hidden=\"true\"\n        className=\"h-6 w-6\"\n        fill=\"none\"\n        stroke=\"currentColor\"\n        strokeWidth={1.5}\n        viewBox=\"0 0 24 24\"\n      >\n        <path\n          d=\"M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09zM18.259 8.715L18 9.75l-.259-1.035a3.375 3.375 0 00-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 002.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 002.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 00-2.456 2.456zM16.894 20.567L16.5 21.75l-.394-1.183a2.25 2.25 0 00-1.423-1.423L13.5 18.75l1.183-.394a2.25 2.25 0 001.423-1.423l.394-1.183.394 1.183a2.25 2.25 0 001.423 1.423l1.183.394-1.183.394a2.25 2.25 0 00-1.423 1.423z\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n        />\n      </svg>\n    ),\n    title: \"Animated\",\n  },\n  {\n    description:\n      \"Free and open source. Install via the shadcn registry with one command.\",\n    icon: (\n      <svg\n        aria-hidden=\"true\"\n        className=\"h-6 w-6\"\n        fill=\"none\"\n        stroke=\"currentColor\"\n        strokeWidth={1.5}\n        viewBox=\"0 0 24 24\"\n      >\n        <path\n          d=\"M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n        />\n      </svg>\n    ),\n    title: \"Open Source\",\n  },\n];\n\nexport function FeaturesGrid() {\n  const shouldReduceMotion = useReducedMotion();\n  const [isHoverDevice, setIsHoverDevice] = useState(false);\n\n  useEffect(() => {\n    const mq = window.matchMedia(\"(hover: hover) and (pointer: fine)\");\n    setIsHoverDevice(mq.matches);\n    const handler = (e: MediaQueryListEvent) => setIsHoverDevice(e.matches);\n    mq.addEventListener(\"change\", handler);\n    return () => mq.removeEventListener(\"change\", handler);\n  }, []);\n\n  return (\n    <section aria-labelledby=\"features-grid-heading\">\n      <div className=\"py-24 md:py-32\">\n        <div className=\"mx-auto max-w-6xl px-6\">\n          <div className=\"mx-auto mb-16 max-w-2xl text-center\">\n            <h2\n              className=\"text-balance font-bold text-3xl tracking-tight md:text-4xl\"\n              id=\"features-grid-heading\"\n            >\n              Everything you need to build\n            </h2>\n            <p className=\"mt-4 text-foreground/70 text-lg\">\n              Production-ready components with animations, accessibility, and\n              TypeScript built in.\n            </p>\n          </div>\n          <ul className=\"grid gap-6 sm:grid-cols-2 lg:grid-cols-3\">\n            {features.map((feature, index) => (\n              <motion.li\n                className={cn(\n                  \"rounded-xl border bg-background p-6 transition-shadow\",\n                  isHoverDevice && !shouldReduceMotion && \"hover:shadow-md\"\n                )}\n                initial={\n                  shouldReduceMotion ? { opacity: 1 } : { opacity: 0, y: 20 }\n                }\n                key={feature.title}\n                transition={\n                  shouldReduceMotion\n                    ? { duration: 0 }\n                    : { ...SPRING, delay: index * 0.05 }\n                }\n                viewport={{ margin: \"-100px\", once: true }}\n                whileHover={\n                  isHoverDevice && !shouldReduceMotion ? { y: -4 } : undefined\n                }\n                whileInView={\n                  shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }\n                }\n              >\n                <div className=\"mb-4 inline-flex rounded-lg bg-foreground/5 p-2.5 text-foreground\">\n                  {feature.icon}\n                </div>\n                <h3 className=\"mb-2 font-semibold text-foreground\">\n                  {feature.title}\n                </h3>\n                <p className=\"text-foreground/70 text-sm leading-relaxed\">\n                  {feature.description}\n                </p>\n              </motion.li>\n            ))}\n          </ul>\n        </div>\n      </div>\n    </section>\n  );\n}\n\nexport default FeaturesGrid;\n","path":"index.tsx","target":"components/smoothui/features-1/index.tsx","type":"registry:block"}],"name":"features-1","registryDependencies":[],"title":"Features 1","type":"registry:block"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"Bento grid features block with asymmetric layout","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { motion, useReducedMotion } from \"motion/react\";\n\nconst SPRING = {\n  bounce: 0.1,\n  duration: 0.25,\n  type: \"spring\" as const,\n};\n\nconst features = [\n  {\n    accent: true,\n    description:\n      \"Real-time analytics dashboard with customizable metrics and beautiful visualizations.\",\n    span: \"col-span-2 row-span-2\",\n    title: \"Smart Analytics\",\n    // The lead cell of a bento is two columns by two rows. Without something to\n    // look at, that is a quarter of the section spent on two lines of text.\n    visual: \"analytics\" as const,\n  },\n  {\n    description:\n      \"Work together in real-time with built-in commenting and sharing.\",\n    span: \"col-span-1\",\n    title: \"Team Collaboration\",\n  },\n  {\n    description: \"RESTful API with comprehensive documentation and SDKs.\",\n    span: \"col-span-1\",\n    title: \"API First\",\n  },\n  {\n    description: \"Lightning-fast delivery from edge locations worldwide.\",\n    span: \"col-span-1\",\n    title: \"Global CDN\",\n  },\n  {\n    description:\n      \"Enterprise-grade security with SOC 2 compliance and encryption.\",\n    span: \"col-span-1\",\n    title: \"Security\",\n  },\n];\n\n/** Bars of a plausible-looking week. Fixed, so the panel never reflows. */\nconst SERIES = [\n  { id: \"w1\", value: 38 },\n  { id: \"w2\", value: 52 },\n  { id: \"w3\", value: 44 },\n  { id: \"w4\", value: 66 },\n  { id: \"w5\", value: 58 },\n  { id: \"w6\", value: 74 },\n  { id: \"w7\", value: 62 },\n  { id: \"w8\", value: 88 },\n  { id: \"w9\", value: 71 },\n  { id: \"w10\", value: 96 },\n  { id: \"w11\", value: 84 },\n  { id: \"w12\", value: 100 },\n];\nconst PEAK_ID = \"w12\";\nconst BAR_STAGGER = 0.03;\n\n/**\n * A small analytics panel for the bento's lead cell.\n *\n * Every element is real markup on the page's own tokens, so it follows the\n * theme into dark mode and stays sharp at any pixel density — and the bars can\n * grow on entry, which is the point of the library it ships with.\n */\nconst AnalyticsPanel = () => {\n  const shouldReduceMotion = useReducedMotion();\n\n  return (\n    <div className=\"flex h-full flex-col gap-4 rounded-xl border bg-background p-5\">\n      <div className=\"flex items-baseline justify-between gap-3\">\n        <div>\n          <p className=\"text-foreground/60 text-xs\">Revenue this month</p>\n          <p className=\"font-semibold text-2xl text-foreground tabular-nums\">\n            $48,219\n          </p>\n        </div>\n        <span className=\"rounded-full bg-foreground/5 px-2 py-0.5 font-medium text-foreground/70 text-xs tabular-nums\">\n          +12.4%\n        </span>\n      </div>\n\n      <div\n        aria-hidden=\"true\"\n        className=\"flex min-h-0 flex-1 items-end gap-1.5 sm:gap-2\"\n      >\n        {SERIES.map(({ id, value }, index) => (\n          <motion.span\n            className={cn(\n              \"flex-1 origin-bottom rounded-t-sm\",\n              id === PEAK_ID ? \"bg-foreground\" : \"bg-foreground/15\"\n            )}\n            initial={shouldReduceMotion ? { scaleY: 1 } : { scaleY: 0 }}\n            key={id}\n            style={{ height: `${value}%` }}\n            transition={\n              shouldReduceMotion\n                ? { duration: 0 }\n                : { ...SPRING, delay: index * BAR_STAGGER }\n            }\n            viewport={{ margin: \"-80px\", once: true }}\n            whileInView={{ scaleY: 1 }}\n          />\n        ))}\n      </div>\n    </div>\n  );\n};\n\nexport function FeaturesBento() {\n  const shouldReduceMotion = useReducedMotion();\n\n  return (\n    <section aria-labelledby=\"features-bento-heading\">\n      <div className=\"py-24 md:py-32\">\n        <div className=\"mx-auto max-w-6xl px-6\">\n          <div className=\"mx-auto mb-16 max-w-2xl text-center\">\n            <h2\n              className=\"text-balance font-bold text-3xl tracking-tight md:text-4xl\"\n              id=\"features-bento-heading\"\n            >\n              Built for modern teams\n            </h2>\n            <p className=\"mt-4 text-foreground/70 text-lg\">\n              A complete platform with everything you need to ship faster.\n            </p>\n          </div>\n          <div className=\"grid auto-rows-[180px] grid-cols-2 gap-4 md:grid-cols-4\">\n            {features.map((feature, index) => (\n              <motion.div\n                className={cn(\n                  \"flex flex-col justify-end overflow-hidden rounded-xl border p-6\",\n                  feature.accent\n                    ? \"bg-foreground/5 ring-1 ring-foreground/10\"\n                    : \"bg-background\",\n                  feature.span\n                )}\n                initial={\n                  shouldReduceMotion\n                    ? { opacity: 1 }\n                    : { opacity: 0, scale: 0.95 }\n                }\n                key={feature.title}\n                transition={\n                  shouldReduceMotion\n                    ? { duration: 0 }\n                    : { ...SPRING, delay: index * 0.05 }\n                }\n                viewport={{ margin: \"-100px\", once: true }}\n                whileInView={\n                  shouldReduceMotion ? { opacity: 1 } : { opacity: 1, scale: 1 }\n                }\n              >\n                {feature.visual === \"analytics\" && (\n                  // Drawn, not photographed. A screenshot would be fixed to one\n                  // theme, blur on a retina screen and weigh more than the whole\n                  // block; markup inherits the page's colours and stays sharp.\n                  <div className=\"-mx-6 -mt-6 mb-4 min-h-0 flex-1 overflow-hidden\">\n                    <AnalyticsPanel />\n                  </div>\n                )}\n                <h3 className=\"mb-1 font-semibold text-foreground\">\n                  {feature.title}\n                </h3>\n                <p className=\"text-foreground/70 text-sm leading-relaxed\">\n                  {feature.description}\n                </p>\n              </motion.div>\n            ))}\n          </div>\n        </div>\n      </div>\n    </section>\n  );\n}\n\nexport default FeaturesBento;\n","path":"index.tsx","target":"components/smoothui/features-2/index.tsx","type":"registry:block"}],"name":"features-2","registryDependencies":[],"title":"Features 2","type":"registry:block"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"Alternating features block with side-by-side layout","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { getImageKitUrl } from \"@/lib/smoothui-data\";\nimport { motion, useReducedMotion } from \"motion/react\";\n\nconst SPRING = {\n  bounce: 0.1,\n  duration: 0.25,\n  type: \"spring\" as const,\n};\n\nconst features = [\n  {\n    description:\n      \"Every component is designed with usability in mind. Clean interfaces that your users will love from the first interaction.\",\n    image: getImageKitUrl(\"/images/designerworking.webp\", {\n      format: \"auto\",\n      quality: 85,\n      width: 800,\n    }),\n    imageAlt: \"Designer working on user interface\",\n    title: \"Intuitive Design\",\n  },\n  {\n    description:\n      \"Optimized animations that only use transform and opacity. Hardware-accelerated rendering for butter-smooth 60fps experiences.\",\n    image: getImageKitUrl(\"/images/hero-smoothui.png\", {\n      format: \"auto\",\n      quality: 85,\n      width: 800,\n    }),\n    imageAlt: \"SmoothUI component showcase\",\n    title: \"Blazing Performance\",\n  },\n  {\n    description:\n      \"Full TypeScript support, comprehensive documentation, and one-command installation via the shadcn registry.\",\n    image: getImageKitUrl(\"/images/hero-example_xertaz.png\", {\n      format: \"auto\",\n      quality: 85,\n      width: 800,\n    }),\n    imageAlt: \"Code example with TypeScript support\",\n    title: \"Developer Experience\",\n  },\n];\n\nexport function FeaturesAlternating() {\n  const shouldReduceMotion = useReducedMotion();\n\n  return (\n    <section aria-labelledby=\"features-alternating-heading\">\n      <div className=\"py-24 md:py-32\">\n        <div className=\"mx-auto max-w-6xl px-6\">\n          <div className=\"mx-auto mb-16 max-w-2xl text-center\">\n            <h2\n              className=\"text-balance font-bold text-3xl tracking-tight md:text-4xl\"\n              id=\"features-alternating-heading\"\n            >\n              Why developers choose us\n            </h2>\n            <p className=\"mt-4 text-foreground/70 text-lg\">\n              Designed for developer productivity and user delight.\n            </p>\n          </div>\n          <div className=\"space-y-24\">\n            {features.map((feature, index) => {\n              const isReversed = index % 2 === 1;\n              const slideDirection = isReversed ? 24 : -24;\n\n              return (\n                <div\n                  className={cn(\n                    \"grid items-center gap-12 md:grid-cols-2\",\n                    isReversed && \"md:[direction:rtl]\"\n                  )}\n                  key={feature.title}\n                >\n                  <motion.div\n                    className=\"md:[direction:ltr]\"\n                    initial={\n                      shouldReduceMotion\n                        ? { opacity: 1 }\n                        : { opacity: 0, x: slideDirection }\n                    }\n                    transition={shouldReduceMotion ? { duration: 0 } : SPRING}\n                    viewport={{ margin: \"-100px\", once: true }}\n                    whileInView={\n                      shouldReduceMotion ? { opacity: 1 } : { opacity: 1, x: 0 }\n                    }\n                  >\n                    <h3 className=\"mb-4 font-bold text-2xl\">{feature.title}</h3>\n                    <p className=\"text-foreground/70 text-lg leading-relaxed\">\n                      {feature.description}\n                    </p>\n                  </motion.div>\n\n                  <motion.div\n                    className=\"md:[direction:ltr]\"\n                    initial={\n                      shouldReduceMotion\n                        ? { opacity: 1 }\n                        : { opacity: 0, x: -slideDirection }\n                    }\n                    transition={shouldReduceMotion ? { duration: 0 } : SPRING}\n                    viewport={{ margin: \"-100px\", once: true }}\n                    whileInView={\n                      shouldReduceMotion ? { opacity: 1 } : { opacity: 1, x: 0 }\n                    }\n                  >\n                    <div className=\"overflow-hidden rounded-xl border shadow-md\">\n                      <img\n                        alt={feature.imageAlt}\n                        className=\"aspect-video h-auto w-full object-cover\"\n                        draggable={false}\n                        src={feature.image}\n                      />\n                    </div>\n                  </motion.div>\n                </div>\n              );\n            })}\n          </div>\n        </div>\n      </div>\n    </section>\n  );\n}\n\nexport default FeaturesAlternating;\n","path":"index.tsx","target":"components/smoothui/features-3/index.tsx","type":"registry:block"}],"name":"features-3","registryDependencies":["https://smoothui.dev/r/data.json"],"title":"Features 3","type":"registry:block"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"Simple footer block with links and social icons","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { motion, useReducedMotion } from \"motion/react\";\n\nconst ANIMATION_DURATION = 0.6;\nconst DELAY_INCREMENT = 0.1;\nconst HOVER_SCALE = 1.1;\nconst TAP_SCALE = 0.9;\nconst DELAY_PRODUCT = DELAY_INCREMENT * 2;\nconst DELAY_COMPANY = DELAY_INCREMENT * 3;\nconst DELAY_SUPPORT = DELAY_INCREMENT * 4;\nconst DELAY_COPYRIGHT = DELAY_INCREMENT * 5;\n\ninterface FooterSimpleProps {\n  companyName?: string;\n  copyright?: string;\n  description?: string;\n  links?: {\n    product?: Array<{ name: string; url: string }>;\n    company?: Array<{ name: string; url: string }>;\n    support?: Array<{ name: string; url: string }>;\n  };\n  social?: {\n    twitter?: string;\n    linkedin?: string;\n    github?: string;\n    discord?: string;\n  };\n}\n\nexport function FooterSimple({\n  companyName = \"Smoothui\",\n  description = \"Build beautiful UIs, effortlessly.\",\n  links = {\n    company: [\n      { name: \"About\", url: \"#about\" },\n      { name: \"Blog\", url: \"#blog\" },\n      { name: \"Careers\", url: \"#careers\" },\n      { name: \"Contact\", url: \"#contact\" },\n    ],\n    product: [\n      { name: \"Features\", url: \"#features\" },\n      { name: \"Pricing\", url: \"#pricing\" },\n      { name: \"Documentation\", url: \"#docs\" },\n      { name: \"API\", url: \"#api\" },\n    ],\n    support: [\n      { name: \"Help Center\", url: \"#help\" },\n      { name: \"Community\", url: \"#community\" },\n      { name: \"Status\", url: \"#status\" },\n      { name: \"Security\", url: \"#security\" },\n    ],\n  },\n  social = {\n    discord: \"https://discord.com\",\n    github: \"https://github.com\",\n    linkedin: \"https://linkedin.com\",\n    twitter: \"https://twitter.com\",\n  },\n  copyright = \"© 2024 Smoothui. All rights reserved.\",\n}: FooterSimpleProps) {\n  const shouldReduceMotion = useReducedMotion();\n  return (\n    <footer className=\"border-border border-t bg-background\">\n      <div className=\"mx-auto max-w-7xl px-6 py-12\">\n        <motion.div\n          animate={shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }}\n          className=\"grid grid-cols-1 gap-8 md:grid-cols-2 lg:grid-cols-5\"\n          initial={shouldReduceMotion ? { opacity: 1 } : { opacity: 0, y: 20 }}\n          transition={\n            shouldReduceMotion\n              ? { duration: 0 }\n              : { duration: ANIMATION_DURATION }\n          }\n          viewport={{ once: true }}\n          whileInView={\n            shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }\n          }\n        >\n          {/* Company Info */}\n          <div className=\"lg:col-span-2\">\n            <motion.div\n              animate={\n                shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }\n              }\n              initial={\n                shouldReduceMotion ? { opacity: 1 } : { opacity: 0, y: 20 }\n              }\n              transition={\n                shouldReduceMotion\n                  ? { duration: 0 }\n                  : {\n                      delay: DELAY_INCREMENT,\n                      duration: ANIMATION_DURATION,\n                    }\n              }\n              viewport={{ once: true }}\n              whileInView={\n                shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }\n              }\n            >\n              <h3 className=\"mb-4 font-bold text-2xl text-foreground\">\n                {companyName}\n              </h3>\n              <p className=\"mb-6 max-w-md text-foreground/70 text-sm\">\n                {description}\n              </p>\n\n              {/* Social Links */}\n              <div className=\"flex gap-4\">\n                {social.twitter ? (\n                  <motion.a\n                    className=\"text-foreground/60 transition-colors hover:text-brand\"\n                    href={social.twitter}\n                    rel=\"noopener noreferrer\"\n                    target=\"_blank\"\n                    whileHover={\n                      shouldReduceMotion ? {} : { scale: HOVER_SCALE }\n                    }\n                    whileTap={shouldReduceMotion ? {} : { scale: TAP_SCALE }}\n                  >\n                    <svg\n                      aria-hidden=\"true\"\n                      className=\"h-5 w-5\"\n                      fill=\"currentColor\"\n                      viewBox=\"0 0 24 24\"\n                    >\n                      <path d=\"M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z\" />\n                    </svg>\n                    <span className=\"sr-only\">Twitter</span>\n                  </motion.a>\n                ) : null}\n                {social.linkedin ? (\n                  <motion.a\n                    className=\"text-foreground/60 transition-colors hover:text-brand\"\n                    href={social.linkedin}\n                    rel=\"noopener noreferrer\"\n                    target=\"_blank\"\n                    whileHover={\n                      shouldReduceMotion ? {} : { scale: HOVER_SCALE }\n                    }\n                    whileTap={shouldReduceMotion ? {} : { scale: TAP_SCALE }}\n                  >\n                    <svg\n                      aria-hidden=\"true\"\n                      className=\"h-5 w-5\"\n                      fill=\"currentColor\"\n                      viewBox=\"0 0 24 24\"\n                    >\n                      <path d=\"M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z\" />\n                    </svg>\n                    <span className=\"sr-only\">LinkedIn</span>\n                  </motion.a>\n                ) : null}\n                {social.github ? (\n                  <motion.a\n                    className=\"text-foreground/60 transition-colors hover:text-brand\"\n                    href={social.github}\n                    rel=\"noopener noreferrer\"\n                    target=\"_blank\"\n                    whileHover={\n                      shouldReduceMotion ? {} : { scale: HOVER_SCALE }\n                    }\n                    whileTap={shouldReduceMotion ? {} : { scale: TAP_SCALE }}\n                  >\n                    <svg\n                      aria-hidden=\"true\"\n                      className=\"h-5 w-5\"\n                      fill=\"currentColor\"\n                      viewBox=\"0 0 24 24\"\n                    >\n                      <path d=\"M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z\" />\n                    </svg>\n                    <span className=\"sr-only\">GitHub</span>\n                  </motion.a>\n                ) : null}\n                {social.discord ? (\n                  <motion.a\n                    className=\"text-foreground/60 transition-colors hover:text-brand\"\n                    href={social.discord}\n                    rel=\"noopener noreferrer\"\n                    target=\"_blank\"\n                    whileHover={\n                      shouldReduceMotion ? {} : { scale: HOVER_SCALE }\n                    }\n                    whileTap={shouldReduceMotion ? {} : { scale: TAP_SCALE }}\n                  >\n                    <svg\n                      aria-hidden=\"true\"\n                      className=\"h-5 w-5\"\n                      fill=\"currentColor\"\n                      viewBox=\"0 0 24 24\"\n                    >\n                      <path d=\"M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z\" />\n                    </svg>\n                    <span className=\"sr-only\">Discord</span>\n                  </motion.a>\n                ) : null}\n              </div>\n            </motion.div>\n          </div>\n\n          {/* Links */}\n          <div className=\"grid grid-cols-1 gap-8 sm:grid-cols-3 lg:col-span-3\">\n            {links.product ? (\n              <motion.div\n                animate={\n                  shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }\n                }\n                initial={\n                  shouldReduceMotion ? { opacity: 1 } : { opacity: 0, y: 20 }\n                }\n                transition={\n                  shouldReduceMotion\n                    ? { duration: 0 }\n                    : { delay: DELAY_PRODUCT, duration: ANIMATION_DURATION }\n                }\n                viewport={{ once: true }}\n                whileInView={\n                  shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }\n                }\n              >\n                <h4 className=\"mb-4 font-semibold text-foreground text-sm uppercase tracking-wide\">\n                  Product\n                </h4>\n                <ul className=\"space-y-3\">\n                  {links.product.map((link) => (\n                    <li key={link.name}>\n                      <a\n                        className=\"text-foreground/70 text-sm transition-colors hover:text-brand\"\n                        href={link.url}\n                      >\n                        {link.name}\n                      </a>\n                    </li>\n                  ))}\n                </ul>\n              </motion.div>\n            ) : null}\n\n            {links.company ? (\n              <motion.div\n                animate={\n                  shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }\n                }\n                initial={\n                  shouldReduceMotion ? { opacity: 1 } : { opacity: 0, y: 20 }\n                }\n                transition={\n                  shouldReduceMotion\n                    ? { duration: 0 }\n                    : { delay: DELAY_COMPANY, duration: ANIMATION_DURATION }\n                }\n                viewport={{ once: true }}\n                whileInView={\n                  shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }\n                }\n              >\n                <h4 className=\"mb-4 font-semibold text-foreground text-sm uppercase tracking-wide\">\n                  Company\n                </h4>\n                <ul className=\"space-y-3\">\n                  {links.company.map((link) => (\n                    <li key={link.name}>\n                      <a\n                        className=\"text-foreground/70 text-sm transition-colors hover:text-brand\"\n                        href={link.url}\n                      >\n                        {link.name}\n                      </a>\n                    </li>\n                  ))}\n                </ul>\n              </motion.div>\n            ) : null}\n\n            {links.support ? (\n              <motion.div\n                animate={\n                  shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }\n                }\n                initial={\n                  shouldReduceMotion ? { opacity: 1 } : { opacity: 0, y: 20 }\n                }\n                transition={\n                  shouldReduceMotion\n                    ? { duration: 0 }\n                    : { delay: DELAY_SUPPORT, duration: ANIMATION_DURATION }\n                }\n                viewport={{ once: true }}\n                whileInView={\n                  shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }\n                }\n              >\n                <h4 className=\"mb-4 font-semibold text-foreground text-sm uppercase tracking-wide\">\n                  Support\n                </h4>\n                <ul className=\"space-y-3\">\n                  {links.support.map((link) => (\n                    <li key={link.name}>\n                      <a\n                        className=\"text-foreground/70 text-sm transition-colors hover:text-brand\"\n                        href={link.url}\n                      >\n                        {link.name}\n                      </a>\n                    </li>\n                  ))}\n                </ul>\n              </motion.div>\n            ) : null}\n          </div>\n        </motion.div>\n\n        {/* Copyright */}\n        <motion.div\n          animate={shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }}\n          className=\"mt-12 border-border border-t pt-8 text-center\"\n          initial={shouldReduceMotion ? { opacity: 1 } : { opacity: 0, y: 20 }}\n          transition={\n            shouldReduceMotion\n              ? { duration: 0 }\n              : { delay: DELAY_COPYRIGHT, duration: ANIMATION_DURATION }\n          }\n          viewport={{ once: true }}\n          whileInView={\n            shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }\n          }\n        >\n          <p className=\"text-foreground/60 text-sm\">{copyright}</p>\n        </motion.div>\n      </div>\n    </footer>\n  );\n}\n\nexport default FooterSimple;\n","path":"index.tsx","target":"components/smoothui/footer-1/index.tsx","type":"registry:block"}],"name":"footer-1","registryDependencies":["https://smoothui.dev/r/tokens.json"],"title":"Footer 1","type":"registry:block"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"Complex footer block with newsletter and extended links","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport SmoothButton from \"@/components/smoothui/smooth-button\";\nimport { motion, useReducedMotion } from \"motion/react\";\n\nconst ANIMATION_DURATION = 0.6;\nconst DELAY_INCREMENT = 0.1;\nconst HOVER_SCALE = 1.1;\nconst TAP_SCALE = 0.9;\nconst DELAY_PRODUCT = DELAY_INCREMENT * 2;\nconst DELAY_COMPANY = DELAY_INCREMENT * 3;\nconst DELAY_SUPPORT = DELAY_INCREMENT * 4;\nconst DELAY_LEGAL = DELAY_INCREMENT * 5;\nconst DELAY_COPYRIGHT = DELAY_INCREMENT * 6;\n\ninterface FooterComplexProps {\n  companyName?: string;\n  copyright?: string;\n  description?: string;\n  links?: {\n    product?: Array<{ name: string; url: string }>;\n    company?: Array<{ name: string; url: string }>;\n    support?: Array<{ name: string; url: string }>;\n    legal?: Array<{ name: string; url: string }>;\n  };\n  newsletter?: {\n    title: string;\n    description: string;\n    placeholder: string;\n    buttonText: string;\n  };\n  social?: {\n    twitter?: string;\n    linkedin?: string;\n    github?: string;\n    discord?: string;\n    youtube?: string;\n  };\n}\n\nexport function FooterComplex({\n  companyName = \"Smoothui\",\n  description = \"Build beautiful UIs, effortlessly. The modern way to create stunning interfaces with smooth animations.\",\n  newsletter = {\n    buttonText: \"Subscribe\",\n    description: \"Get the latest news and updates delivered to your inbox.\",\n    placeholder: \"Enter your email\",\n    title: \"Stay updated\",\n  },\n  links = {\n    company: [\n      { name: \"About Us\", url: \"#about\" },\n      { name: \"Blog\", url: \"#blog\" },\n      { name: \"Careers\", url: \"#careers\" },\n      { name: \"Press Kit\", url: \"#press\" },\n      { name: \"Contact\", url: \"#contact\" },\n    ],\n    legal: [\n      { name: \"Privacy Policy\", url: \"#privacy\" },\n      { name: \"Terms of Service\", url: \"#terms\" },\n      { name: \"Cookie Policy\", url: \"#cookies\" },\n      { name: \"GDPR\", url: \"#gdpr\" },\n    ],\n    product: [\n      { name: \"Features\", url: \"#features\" },\n      { name: \"Pricing\", url: \"#pricing\" },\n      { name: \"Documentation\", url: \"#docs\" },\n      { name: \"API Reference\", url: \"#api\" },\n      { name: \"Changelog\", url: \"#changelog\" },\n    ],\n    support: [\n      { name: \"Help Center\", url: \"#help\" },\n      { name: \"Community\", url: \"#community\" },\n      { name: \"Status Page\", url: \"#status\" },\n      { name: \"Security\", url: \"#security\" },\n      { name: \"Bug Reports\", url: \"#bugs\" },\n    ],\n  },\n  social = {\n    discord: \"https://discord.com\",\n    github: \"https://github.com\",\n    linkedin: \"https://linkedin.com\",\n    twitter: \"https://twitter.com\",\n    youtube: \"https://youtube.com\",\n  },\n  copyright = \"© 2024 Smoothui. All rights reserved.\",\n}: FooterComplexProps) {\n  const shouldReduceMotion = useReducedMotion();\n  const socialHover = shouldReduceMotion ? {} : { scale: HOVER_SCALE };\n  const socialTap = shouldReduceMotion ? {} : { scale: TAP_SCALE };\n  return (\n    <footer className=\"border-border border-t bg-background\">\n      <div className=\"mx-auto max-w-7xl px-6 py-16\">\n        <motion.div\n          animate={shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }}\n          className=\"grid grid-cols-1 gap-12 lg:grid-cols-12\"\n          initial={shouldReduceMotion ? { opacity: 1 } : { opacity: 0, y: 20 }}\n          transition={\n            shouldReduceMotion\n              ? { duration: 0 }\n              : { duration: ANIMATION_DURATION }\n          }\n          viewport={{ once: true }}\n          whileInView={\n            shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }\n          }\n        >\n          {/* Company Info & Newsletter */}\n          <div className=\"lg:col-span-5\">\n            <motion.div\n              animate={\n                shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }\n              }\n              initial={\n                shouldReduceMotion ? { opacity: 1 } : { opacity: 0, y: 20 }\n              }\n              transition={\n                shouldReduceMotion\n                  ? { duration: 0 }\n                  : { delay: DELAY_INCREMENT, duration: ANIMATION_DURATION }\n              }\n              viewport={{ once: true }}\n              whileInView={\n                shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }\n              }\n            >\n              <h3 className=\"mb-4 font-bold text-2xl text-foreground\">\n                {companyName}\n              </h3>\n              <p className=\"mb-8 max-w-md text-foreground/70 text-sm leading-relaxed\">\n                {description}\n              </p>\n\n              {/* Newsletter */}\n              <div className=\"mb-8\">\n                <h4 className=\"mb-2 font-semibold text-foreground text-lg\">\n                  {newsletter.title}\n                </h4>\n                <p className=\"mb-4 text-foreground/70 text-sm\">\n                  {newsletter.description}\n                </p>\n                <div className=\"flex gap-2\">\n                  <input\n                    className=\"flex-1 rounded-lg border border-border bg-background px-4 py-2 text-sm placeholder:text-foreground/50 focus:border-brand focus:outline-none focus:ring-2 focus:ring-brand/20\"\n                    placeholder={newsletter.placeholder}\n                    type=\"email\"\n                  />\n                  <SmoothButton variant=\"candy\">\n                    {newsletter.buttonText}\n                  </SmoothButton>\n                </div>\n              </div>\n\n              {/* Social Links */}\n              <div className=\"flex gap-4\">\n                {social.twitter ? (\n                  <motion.a\n                    aria-label=\"Twitter\"\n                    className=\"text-foreground/60 transition-colors hover:text-brand\"\n                    href={social.twitter}\n                    rel=\"noopener noreferrer\"\n                    target=\"_blank\"\n                    whileHover={socialHover}\n                    whileTap={socialTap}\n                  >\n                    <svg\n                      aria-hidden=\"true\"\n                      className=\"h-5 w-5\"\n                      fill=\"currentColor\"\n                      viewBox=\"0 0 24 24\"\n                    >\n                      <path d=\"M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z\" />\n                    </svg>\n                    <span className=\"sr-only\">Twitter</span>\n                  </motion.a>\n                ) : null}\n                {social.linkedin ? (\n                  <motion.a\n                    className=\"text-foreground/60 transition-colors hover:text-brand\"\n                    href={social.linkedin}\n                    rel=\"noopener noreferrer\"\n                    target=\"_blank\"\n                    whileHover={socialHover}\n                    whileTap={socialTap}\n                  >\n                    <svg\n                      aria-hidden=\"true\"\n                      className=\"h-5 w-5\"\n                      fill=\"currentColor\"\n                      viewBox=\"0 0 24 24\"\n                    >\n                      <path d=\"M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z\" />\n                    </svg>\n                    <span className=\"sr-only\">LinkedIn</span>\n                  </motion.a>\n                ) : null}\n                {social.github ? (\n                  <motion.a\n                    className=\"text-foreground/60 transition-colors hover:text-brand\"\n                    href={social.github}\n                    rel=\"noopener noreferrer\"\n                    target=\"_blank\"\n                    whileHover={socialHover}\n                    whileTap={socialTap}\n                  >\n                    <svg\n                      aria-hidden=\"true\"\n                      className=\"h-5 w-5\"\n                      fill=\"currentColor\"\n                      viewBox=\"0 0 24 24\"\n                    >\n                      <path d=\"M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z\" />\n                    </svg>\n                    <span className=\"sr-only\">GitHub</span>\n                  </motion.a>\n                ) : null}\n                {social.discord ? (\n                  <motion.a\n                    className=\"text-foreground/60 transition-colors hover:text-brand\"\n                    href={social.discord}\n                    rel=\"noopener noreferrer\"\n                    target=\"_blank\"\n                    whileHover={socialHover}\n                    whileTap={socialTap}\n                  >\n                    <svg\n                      aria-hidden=\"true\"\n                      className=\"h-5 w-5\"\n                      fill=\"currentColor\"\n                      viewBox=\"0 0 24 24\"\n                    >\n                      <path d=\"M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z\" />\n                    </svg>\n                    <span className=\"sr-only\">Discord</span>\n                  </motion.a>\n                ) : null}\n                {social.youtube ? (\n                  <motion.a\n                    className=\"text-foreground/60 transition-colors hover:text-brand\"\n                    href={social.youtube}\n                    rel=\"noopener noreferrer\"\n                    target=\"_blank\"\n                    whileHover={socialHover}\n                    whileTap={socialTap}\n                  >\n                    <svg\n                      aria-hidden=\"true\"\n                      className=\"h-5 w-5\"\n                      fill=\"currentColor\"\n                      viewBox=\"0 0 24 24\"\n                    >\n                      <path d=\"M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z\" />\n                    </svg>\n                    <span className=\"sr-only\">YouTube</span>\n                  </motion.a>\n                ) : null}\n              </div>\n            </motion.div>\n          </div>\n\n          {/* Links Grid */}\n          <div className=\"grid grid-cols-2 gap-8 lg:col-span-7 lg:grid-cols-4\">\n            {links.product ? (\n              <motion.div\n                animate={\n                  shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }\n                }\n                initial={\n                  shouldReduceMotion ? { opacity: 1 } : { opacity: 0, y: 20 }\n                }\n                transition={\n                  shouldReduceMotion\n                    ? { duration: 0 }\n                    : { delay: DELAY_PRODUCT, duration: ANIMATION_DURATION }\n                }\n                viewport={{ once: true }}\n                whileInView={\n                  shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }\n                }\n              >\n                <h4 className=\"mb-4 font-semibold text-foreground text-sm uppercase tracking-wide\">\n                  Product\n                </h4>\n                <ul className=\"space-y-3\">\n                  {links.product.map((link) => (\n                    <li key={link.name}>\n                      <a\n                        className=\"text-foreground/70 text-sm transition-colors hover:text-brand\"\n                        href={link.url}\n                      >\n                        {link.name}\n                      </a>\n                    </li>\n                  ))}\n                </ul>\n              </motion.div>\n            ) : null}\n\n            {links.company ? (\n              <motion.div\n                animate={\n                  shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }\n                }\n                initial={\n                  shouldReduceMotion ? { opacity: 1 } : { opacity: 0, y: 20 }\n                }\n                transition={\n                  shouldReduceMotion\n                    ? { duration: 0 }\n                    : { delay: DELAY_COMPANY, duration: ANIMATION_DURATION }\n                }\n                viewport={{ once: true }}\n                whileInView={\n                  shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }\n                }\n              >\n                <h4 className=\"mb-4 font-semibold text-foreground text-sm uppercase tracking-wide\">\n                  Company\n                </h4>\n                <ul className=\"space-y-3\">\n                  {links.company.map((link) => (\n                    <li key={link.name}>\n                      <a\n                        className=\"text-foreground/70 text-sm transition-colors hover:text-brand\"\n                        href={link.url}\n                      >\n                        {link.name}\n                      </a>\n                    </li>\n                  ))}\n                </ul>\n              </motion.div>\n            ) : null}\n\n            {links.support ? (\n              <motion.div\n                animate={\n                  shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }\n                }\n                initial={\n                  shouldReduceMotion ? { opacity: 1 } : { opacity: 0, y: 20 }\n                }\n                transition={\n                  shouldReduceMotion\n                    ? { duration: 0 }\n                    : { delay: DELAY_SUPPORT, duration: ANIMATION_DURATION }\n                }\n                viewport={{ once: true }}\n                whileInView={\n                  shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }\n                }\n              >\n                <h4 className=\"mb-4 font-semibold text-foreground text-sm uppercase tracking-wide\">\n                  Support\n                </h4>\n                <ul className=\"space-y-3\">\n                  {links.support.map((link) => (\n                    <li key={link.name}>\n                      <a\n                        className=\"text-foreground/70 text-sm transition-colors hover:text-brand\"\n                        href={link.url}\n                      >\n                        {link.name}\n                      </a>\n                    </li>\n                  ))}\n                </ul>\n              </motion.div>\n            ) : null}\n\n            {links.legal ? (\n              <motion.div\n                animate={\n                  shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }\n                }\n                initial={\n                  shouldReduceMotion ? { opacity: 1 } : { opacity: 0, y: 20 }\n                }\n                transition={\n                  shouldReduceMotion\n                    ? { duration: 0 }\n                    : { delay: DELAY_LEGAL, duration: ANIMATION_DURATION }\n                }\n                viewport={{ once: true }}\n                whileInView={\n                  shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }\n                }\n              >\n                <h4 className=\"mb-4 font-semibold text-foreground text-sm uppercase tracking-wide\">\n                  Legal\n                </h4>\n                <ul className=\"space-y-3\">\n                  {links.legal.map((link) => (\n                    <li key={link.name}>\n                      <a\n                        className=\"text-foreground/70 text-sm transition-colors hover:text-brand\"\n                        href={link.url}\n                      >\n                        {link.name}\n                      </a>\n                    </li>\n                  ))}\n                </ul>\n              </motion.div>\n            ) : null}\n          </div>\n        </motion.div>\n\n        {/* Copyright */}\n        <motion.div\n          animate={{ opacity: 1, y: 0 }}\n          className=\"mt-12 border-border border-t pt-8 text-center\"\n          initial={{ opacity: 0, y: 20 }}\n          transition={{\n            delay: DELAY_COPYRIGHT,\n            duration: ANIMATION_DURATION,\n          }}\n          viewport={{ once: true }}\n          whileInView={{ opacity: 1, y: 0 }}\n        >\n          <p className=\"text-foreground/60 text-sm\">{copyright}</p>\n        </motion.div>\n      </div>\n    </footer>\n  );\n}\n\nexport default FooterComplex;\n","path":"index.tsx","target":"components/smoothui/footer-2/index.tsx","type":"registry:block"}],"name":"footer-2","registryDependencies":["https://smoothui.dev/r/smooth-button.json","https://smoothui.dev/r/tokens.json"],"title":"Footer 2","type":"registry:block"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["lucide-react","motion"],"description":"Mega footer with newsletter and social links","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport SmoothButton from \"@/components/smoothui/smooth-button\";\nimport { Github, Linkedin, Twitter, Youtube } from \"lucide-react\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport type { ReactNode } from \"react\";\n\nconst ANIMATION_DURATION = 0.25;\nconst DELAY_INCREMENT = 0.05;\nconst HOVER_SCALE = 1.1;\nconst TAP_SCALE = 0.95;\nconst DELAY_NEWSLETTER = 6;\nconst DELAY_BOTTOM = 7;\n\nexport interface FooterMegaProps {\n  columns?: Array<{\n    title: string;\n    links: Array<{ label: string; href: string }>;\n  }>;\n  copyright?: string;\n  description?: string;\n  logo?: ReactNode;\n  newsletter?: {\n    title: string;\n    description: string;\n    placeholder: string;\n    buttonText: string;\n  };\n  socialLinks?: Array<{\n    icon: ReactNode;\n    href: string;\n    label: string;\n  }>;\n}\n\nconst defaultColumns = [\n  {\n    links: [\n      { href: \"#features\", label: \"Features\" },\n      { href: \"#pricing\", label: \"Pricing\" },\n      { href: \"#changelog\", label: \"Changelog\" },\n      { href: \"#docs\", label: \"Documentation\" },\n    ],\n    title: \"Product\",\n  },\n  {\n    links: [\n      { href: \"#about\", label: \"About\" },\n      { href: \"#blog\", label: \"Blog\" },\n      { href: \"#careers\", label: \"Careers\" },\n      { href: \"#contact\", label: \"Contact\" },\n    ],\n    title: \"Company\",\n  },\n  {\n    links: [\n      { href: \"#community\", label: \"Community\" },\n      { href: \"#github\", label: \"GitHub\" },\n      { href: \"#discord\", label: \"Discord\" },\n      { href: \"#help\", label: \"Help Center\" },\n    ],\n    title: \"Resources\",\n  },\n  {\n    links: [\n      { href: \"#privacy\", label: \"Privacy Policy\" },\n      { href: \"#terms\", label: \"Terms of Service\" },\n      { href: \"#cookies\", label: \"Cookie Policy\" },\n    ],\n    title: \"Legal\",\n  },\n];\n\nconst defaultSocialLinks = [\n  {\n    href: \"https://twitter.com\",\n    icon: <Twitter className=\"h-5 w-5\" />,\n    label: \"Twitter\",\n  },\n  {\n    href: \"https://github.com\",\n    icon: <Github className=\"h-5 w-5\" />,\n    label: \"GitHub\",\n  },\n  {\n    href: \"https://linkedin.com\",\n    icon: <Linkedin className=\"h-5 w-5\" />,\n    label: \"LinkedIn\",\n  },\n  {\n    href: \"https://youtube.com\",\n    icon: <Youtube className=\"h-5 w-5\" />,\n    label: \"YouTube\",\n  },\n];\n\nexport const FooterMega = ({\n  logo = <span className=\"font-bold text-2xl text-foreground\">SmoothUI</span>,\n  description = \"Beautiful animated React components for building modern user interfaces with smooth animations and delightful interactions.\",\n  columns = defaultColumns,\n  newsletter = {\n    buttonText: \"Subscribe\",\n    description: \"Get the latest updates and news delivered to your inbox.\",\n    placeholder: \"Enter your email\",\n    title: \"Subscribe to our newsletter\",\n  },\n  socialLinks = defaultSocialLinks,\n  copyright = \"© 2024 SmoothUI. All rights reserved.\",\n}: FooterMegaProps) => {\n  const shouldReduceMotion = useReducedMotion();\n\n  const getAnimationProps = (delay = 0) => {\n    if (shouldReduceMotion) {\n      return {\n        animate: { opacity: 1 },\n        initial: { opacity: 1 },\n        transition: { duration: 0 },\n      };\n    }\n\n    return {\n      initial: { opacity: 0, y: 20 },\n      transition: {\n        bounce: 0.1,\n        delay,\n        duration: ANIMATION_DURATION,\n        type: \"spring\" as const,\n      },\n      viewport: { once: true },\n      whileInView: { opacity: 1, y: 0 },\n    };\n  };\n\n  const getHoverProps = () => {\n    if (shouldReduceMotion) {\n      return {};\n    }\n\n    return {\n      whileHover: { scale: HOVER_SCALE },\n      whileTap: { scale: TAP_SCALE },\n    };\n  };\n\n  return (\n    <footer className=\"border-border border-t bg-background\">\n      <div className=\"mx-auto max-w-7xl px-6 py-16\">\n        {/* Top Section: Logo, Description, Columns, Newsletter */}\n        <motion.div\n          {...getAnimationProps()}\n          className=\"grid grid-cols-1 gap-12 lg:grid-cols-12\"\n        >\n          {/* Logo and Description */}\n          <motion.div\n            {...getAnimationProps(DELAY_INCREMENT)}\n            className=\"lg:col-span-3\"\n          >\n            <div className=\"mb-4\">{logo}</div>\n            <p className=\"text-foreground/70 text-sm leading-relaxed\">\n              {description}\n            </p>\n          </motion.div>\n\n          {/* Link Columns */}\n          <div className=\"grid grid-cols-2 gap-8 sm:grid-cols-4 lg:col-span-5\">\n            {columns.map((column, columnIndex) => (\n              <motion.div\n                key={column.title}\n                {...getAnimationProps(DELAY_INCREMENT * (columnIndex + 2))}\n              >\n                <h4 className=\"mb-4 font-semibold text-foreground text-sm uppercase tracking-wide\">\n                  {column.title}\n                </h4>\n                <ul className=\"space-y-3\">\n                  {column.links.map((link) => (\n                    <li key={link.label}>\n                      <a\n                        className=\"text-foreground/70 text-sm transition-colors hover:text-brand\"\n                        href={link.href}\n                      >\n                        {link.label}\n                      </a>\n                    </li>\n                  ))}\n                </ul>\n              </motion.div>\n            ))}\n          </div>\n\n          {/* Newsletter */}\n          <motion.div\n            {...getAnimationProps(DELAY_INCREMENT * DELAY_NEWSLETTER)}\n            className=\"lg:col-span-4\"\n          >\n            <h4 className=\"mb-2 font-semibold text-foreground text-lg\">\n              {newsletter.title}\n            </h4>\n            <p className=\"mb-4 text-foreground/70 text-sm\">\n              {newsletter.description}\n            </p>\n            <form\n              className=\"flex flex-col gap-3 sm:flex-row\"\n              onSubmit={(e) => e.preventDefault()}\n            >\n              <input\n                aria-label=\"Email address\"\n                className=\"flex-1 rounded-lg border border-border bg-background px-4 py-2.5 text-sm placeholder:text-foreground/50 focus:border-brand focus:outline-none focus:ring-2 focus:ring-brand/20\"\n                placeholder={newsletter.placeholder}\n                type=\"email\"\n              />\n              <SmoothButton type=\"submit\" variant=\"candy\">\n                {newsletter.buttonText}\n              </SmoothButton>\n            </form>\n          </motion.div>\n        </motion.div>\n\n        {/* Bottom Section: Copyright and Social Links */}\n        <motion.div\n          {...getAnimationProps(DELAY_INCREMENT * DELAY_BOTTOM)}\n          className=\"mt-12 flex flex-col items-center justify-between gap-6 border-border border-t pt-8 sm:flex-row\"\n        >\n          <p className=\"text-foreground/60 text-sm\">{copyright}</p>\n\n          <div className=\"flex gap-4\">\n            {socialLinks.map((social) => (\n              <motion.a\n                aria-label={social.label}\n                className=\"text-foreground/60 transition-colors hover:text-brand\"\n                href={social.href}\n                key={social.label}\n                rel=\"noopener noreferrer\"\n                target=\"_blank\"\n                {...getHoverProps()}\n              >\n                {social.icon}\n                <span className=\"sr-only\">{social.label}</span>\n              </motion.a>\n            ))}\n          </div>\n        </motion.div>\n      </div>\n    </footer>\n  );\n};\n\nexport default FooterMega;\n","path":"index.tsx","target":"components/smoothui/footer-3/index.tsx","type":"registry:block"}],"name":"footer-3","registryDependencies":["https://smoothui.dev/r/smooth-button.json","https://smoothui.dev/r/tokens.json"],"title":"Footer 3","type":"registry:block"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["lucide-react","motion"],"description":"Minimal single-row footer","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { Github, Linkedin, Twitter } from \"lucide-react\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport type { ReactNode } from \"react\";\n\nconst ANIMATION_DURATION = 0.25;\nconst HOVER_SCALE = 1.08;\nconst TAP_SCALE = 0.95;\n\nexport interface FooterMinimalProps {\n  copyright?: string;\n  links?: Array<{ label: string; href: string }>;\n  logo?: ReactNode;\n  socialLinks?: Array<{\n    icon: ReactNode;\n    href: string;\n    label: string;\n  }>;\n}\n\nconst defaultLinks = [\n  { href: \"#privacy\", label: \"Privacy\" },\n  { href: \"#terms\", label: \"Terms\" },\n  { href: \"#contact\", label: \"Contact\" },\n];\n\nconst defaultSocialLinks = [\n  {\n    href: \"https://twitter.com\",\n    icon: <Twitter className=\"h-5 w-5\" />,\n    label: \"Twitter\",\n  },\n  {\n    href: \"https://github.com\",\n    icon: <Github className=\"h-5 w-5\" />,\n    label: \"GitHub\",\n  },\n  {\n    href: \"https://linkedin.com\",\n    icon: <Linkedin className=\"h-5 w-5\" />,\n    label: \"LinkedIn\",\n  },\n];\n\nexport const FooterMinimal = ({\n  logo = <span className=\"font-bold text-foreground text-xl\">SmoothUI</span>,\n  links = defaultLinks,\n  socialLinks = defaultSocialLinks,\n  copyright = \"© 2024 SmoothUI\",\n}: FooterMinimalProps) => {\n  const shouldReduceMotion = useReducedMotion();\n\n  const getAnimationProps = () => {\n    if (shouldReduceMotion) {\n      return {\n        animate: { opacity: 1 },\n        initial: { opacity: 1 },\n        transition: { duration: 0 },\n      };\n    }\n\n    return {\n      initial: { opacity: 0, y: 10 },\n      transition: {\n        bounce: 0.1,\n        duration: ANIMATION_DURATION,\n        type: \"spring\" as const,\n      },\n      viewport: { once: true },\n      whileInView: { opacity: 1, y: 0 },\n    };\n  };\n\n  const getHoverProps = () => {\n    if (shouldReduceMotion) {\n      return {};\n    }\n\n    return {\n      whileHover: { scale: HOVER_SCALE },\n      whileTap: { scale: TAP_SCALE },\n    };\n  };\n\n  return (\n    <footer className=\"border-border border-t bg-background\">\n      <div className=\"mx-auto max-w-7xl px-6 py-6\">\n        <motion.div\n          {...getAnimationProps()}\n          className=\"flex flex-col items-center justify-between gap-6 md:flex-row\"\n        >\n          {/* Logo and Copyright */}\n          <div className=\"flex items-center gap-3\">\n            <div>{logo}</div>\n            <span className=\"text-foreground/50\">|</span>\n            <span className=\"text-foreground/60 text-sm\">{copyright}</span>\n          </div>\n\n          {/* Navigation Links */}\n          <nav className=\"flex items-center gap-6\">\n            {links.map((link) => (\n              <a\n                className=\"relative text-foreground/70 text-sm transition-colors after:absolute after:-bottom-0.5 after:left-0 after:h-px after:w-0 after:bg-foreground after:transition-all after:duration-200 hover:text-foreground hover:after:w-full\"\n                href={link.href}\n                key={link.label}\n              >\n                {link.label}\n              </a>\n            ))}\n          </nav>\n\n          {/* Social Links */}\n          <div className=\"flex items-center gap-4\">\n            {socialLinks.map((social) => (\n              <motion.a\n                aria-label={social.label}\n                className=\"text-foreground/60 transition-colors hover:text-brand\"\n                href={social.href}\n                key={social.label}\n                rel=\"noopener noreferrer\"\n                target=\"_blank\"\n                {...getHoverProps()}\n              >\n                {social.icon}\n                <span className=\"sr-only\">{social.label}</span>\n              </motion.a>\n            ))}\n          </div>\n        </motion.div>\n      </div>\n    </footer>\n  );\n};\n\nexport default FooterMinimal;\n","path":"index.tsx","target":"components/smoothui/footer-4/index.tsx","type":"registry:block"}],"name":"footer-4","registryDependencies":["https://smoothui.dev/r/tokens.json"],"title":"Footer 4","type":"registry:block"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["lucide-react","motion"],"description":"Modern header block with navigation and mobile menu","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { ExternalLink } from \"lucide-react\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { AnimatedGroup, AnimatedText, Button, HeroHeader } from \"@/components/smoothui/shared\";\nimport styles from \"./hero-grid.module.css\";\n\nconst CELL_SIZE = 120; // px\nconst COLORS = [\n  \"oklch(0.72 0.2 352.53)\", // blue\n  \"#A764FF\",\n  \"#4B94FD\",\n  \"#FD4B4E\",\n  \"#FF8743\",\n];\n\nfunction getRandomColor() {\n  return COLORS[Math.floor(Math.random() * COLORS.length)];\n}\n\nfunction SubGrid() {\n  const [cellColors, setCellColors] = useState<(string | null)[]>([\n    null,\n    null,\n    null,\n    null,\n  ]);\n  // Add refs for leave timeouts\n  const leaveTimeouts = useRef<(NodeJS.Timeout | null)[]>([\n    null,\n    null,\n    null,\n    null,\n  ]);\n\n  function handleHover(cellIdx: number) {\n    // Clear any pending timeout for this cell\n    const timeout = leaveTimeouts.current[cellIdx];\n    if (timeout) {\n      clearTimeout(timeout);\n      leaveTimeouts.current[cellIdx] = null;\n    }\n    setCellColors((prev) =>\n      prev.map((c, i) => (i === cellIdx ? getRandomColor() : c))\n    );\n  }\n  function handleLeave(cellIdx: number) {\n    // Add a small delay before removing the color\n    leaveTimeouts.current[cellIdx] = setTimeout(() => {\n      setCellColors((prev) => prev.map((c, i) => (i === cellIdx ? null : c)));\n      leaveTimeouts.current[cellIdx] = null;\n    }, 120);\n  }\n  // Cleanup on unmount\n  useEffect(\n    () => () => {\n      for (const t of leaveTimeouts.current) {\n        if (t) {\n          clearTimeout(t);\n        }\n      }\n    },\n    []\n  );\n\n  return (\n    <div className={styles.subgrid} style={{ pointerEvents: \"none\" }}>\n      {[0, 1, 2, 3].map((cellIdx) => (\n        <button\n          className={styles.cell}\n          key={cellIdx}\n          onMouseEnter={() => handleHover(cellIdx)}\n          onMouseLeave={() => handleLeave(cellIdx)}\n          style={{\n            background: cellColors[cellIdx] || \"transparent\",\n            pointerEvents: \"auto\",\n          }}\n          type=\"button\"\n        />\n      ))}\n    </div>\n  );\n}\n\nfunction InteractiveGrid() {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const [grid, setGrid] = useState({ columns: 0, rows: 0 });\n\n  useEffect(() => {\n    function updateGrid() {\n      if (containerRef.current) {\n        const { width, height } = containerRef.current.getBoundingClientRect();\n        setGrid({\n          columns: Math.ceil(width / CELL_SIZE),\n          rows: Math.ceil(height / CELL_SIZE),\n        });\n      }\n    }\n    updateGrid();\n    window.addEventListener(\"resize\", updateGrid);\n    return () => window.removeEventListener(\"resize\", updateGrid);\n  }, []);\n\n  const total = grid.columns * grid.rows;\n\n  return (\n    <div\n      aria-hidden=\"true\"\n      className=\"pointer-events-none absolute inset-0 z-0\"\n      ref={containerRef}\n      style={{ height: \"100%\", width: \"100%\" }}\n    >\n      <div\n        className={styles.mainGrid}\n        style={\n          {\n            \"--grid-cell-size\": `${CELL_SIZE}px`,\n            gridTemplateColumns: `repeat(${grid.columns}, 1fr)`,\n            gridTemplateRows: `repeat(${grid.rows}, 1fr)`,\n            height: \"100%\",\n            width: \"100%\",\n          } as React.CSSProperties\n        }\n      >\n        {Array.from({ length: total }, (_, idx) => (\n          <SubGrid key={`subgrid-${grid.columns}-${grid.rows}-${idx}`} />\n        ))}\n      </div>\n    </div>\n  );\n}\n\nexport function HeroGrid() {\n  return (\n    <div className=\"relative\">\n      <HeroHeader />\n      <main>\n        <section className=\"relative overflow-hidden py-36\">\n          {/* Interactive animated grid background */}\n          <InteractiveGrid />\n          <AnimatedGroup\n            className=\"pointer-events-none flex flex-col items-center gap-6 text-center\"\n            preset=\"blur-slide\"\n          >\n            <div>\n              <AnimatedText\n                as=\"h1\"\n                className=\"mb-6 text-pretty font-bold text-2xl tracking-tight lg:text-5xl\"\n              >\n                Build your next project with{\" \"}\n                <span className=\"text-brand\">Smoothui</span>\n              </AnimatedText>\n              <AnimatedText\n                as=\"p\"\n                className=\"mx-auto max-w-3xl text-muted-foreground lg:text-xl\"\n                delay={0.15}\n              >\n                Smoothui gives you the building blocks to create stunning,\n                animated interfaces in minutes.\n              </AnimatedText>\n            </div>\n            <AnimatedGroup\n              className=\"pointer-events-auto mt-6 flex justify-center gap-3\"\n              preset=\"slide\"\n            >\n              <Button\n                className=\"shadow-sm transition-shadow hover:shadow\"\n                variant=\"outline\"\n              >\n                Get Started\n              </Button>\n              <Button className=\"group\" variant=\"candy\">\n                Learn more{\" \"}\n                <ExternalLink className=\"ml-2 h-4 transition-transform group-hover:translate-x-0.5\" />\n              </Button>\n            </AnimatedGroup>\n          </AnimatedGroup>\n        </section>\n      </main>\n    </div>\n  );\n}\n\nexport default HeroGrid;\n","path":"index.tsx","target":"components/smoothui/header-1/index.tsx","type":"registry:block"},{"content":"/* @reference \"../../../../app/styles/smoothui.css\"; */\n\n.mainGrid {\n  position: absolute;\n  top: 0;\n  left: 0;\n  display: grid;\n  width: 100%;\n  height: 100%;\n  pointer-events: auto;\n  border-top: 1px solid var(--color-smooth-200);\n  border-right: 1px solid var(--color-smooth-200);\n  opacity: 1;\n  transition: opacity 300ms;\n  will-change: transform;\n}\n\n.subgrid {\n  position: relative;\n  display: grid;\n  grid-template-rows: repeat(2, 1fr);\n  grid-template-columns: repeat(2, 1fr);\n  width: var(--grid-cell-size);\n  height: var(--grid-cell-size);\n  pointer-events: auto;\n  user-select: none;\n  border-top: 1px solid var(--color-smooth-200);\n  border-right: 1px solid var(--color-smooth-200);\n}\n\n.cell {\n  width: 100%;\n  height: 100%;\n  pointer-events: auto;\n  transition: background 3000ms cubic-bezier(0.23, 1, 0.32, 1);\n}\n\n.cell:nth-child(1) {\n  border-right: 1px solid var(--color-smooth-200);\n}\n.cell:nth-child(3) {\n  border-top: 1px solid var(--color-smooth-200);\n  border-right: 1px solid var(--color-smooth-200);\n}\n.cell:nth-child(4) {\n  border-top: 1px solid var(--color-smooth-200);\n}\n\n/* Reduce overlay size of hero text/button containers */\n.heroContent {\n  display: flex;\n  flex-direction: column;\n  align-items: flex-start;\n  max-width: 600px;\n  padding: 0;\n  margin: 0 auto;\n  background: none;\n  box-shadow: none;\n}\n\n.heroButtons {\n  display: flex;\n  gap: 1rem;\n  width: auto;\n  padding: 0;\n  margin-top: 1.5rem;\n  background: none;\n  box-shadow: none;\n}\n","path":"hero-grid.module.css","target":"components/smoothui/header-1/hero-grid.module.css","type":"registry:block"}],"name":"header-1","registryDependencies":["https://smoothui.dev/r/shared.json","https://smoothui.dev/r/tokens.json"],"title":"Header 1","type":"registry:block"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["lucide-react","motion"],"description":"Premium header block with gradient logo and smooth animations","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { getImageKitUrl } from \"@/lib/smoothui-data\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { AnimatedGroup, AnimatedText, Button, HeroHeader } from \"@/components/smoothui/shared\";\n\ninterface HeroProductProps {\n  badgeText?: string;\n  description?: string;\n  heading?: string;\n  imageAlt?: string;\n  imageSrc?: string;\n  primaryButton?: {\n    text: string;\n    url: string;\n  };\n  secondaryButton?: {\n    text: string;\n    url: string;\n  };\n}\n\nexport function HeroProduct({\n  badgeText = \"New animation library\",\n  heading = \"Build Beautiful UIs, Effortlessly\",\n  description = \"Craft. Build. Ship Modern Websites With Smooth Animations.\",\n  primaryButton = {\n    text: \"Start Building\",\n    url: \"#link\",\n  },\n  secondaryButton = {\n    text: \"Watch Video\",\n    url: \"#link\",\n  },\n  imageSrc = getImageKitUrl(\"/images/hero-smoothui.png\", {\n    format: \"auto\",\n    quality: 85,\n    width: 1200,\n  }),\n  imageAlt = \"SmoothUI desktop application interface\",\n}: HeroProductProps) {\n  const shouldReduceMotion = useReducedMotion();\n  return (\n    <div className=\"relative\">\n      <HeroHeader />\n      <main>\n        <motion.section\n          animate={\n            shouldReduceMotion ? { opacity: 1 } : { opacity: 1, scale: 1 }\n          }\n          className=\"relative overflow-hidden\"\n          initial={\n            shouldReduceMotion ? { opacity: 1 } : { opacity: 0, scale: 1.02 }\n          }\n          transition={\n            shouldReduceMotion\n              ? { duration: 0 }\n              : {\n                  duration: 0.8,\n                  ease: [0.25, 0.46, 0.45, 0.94], // ease-out-quad\n                }\n          }\n        >\n          <div className=\"py-20 md:py-36\">\n            <div className=\"relative z-10 mx-auto max-w-5xl px-6 text-center\">\n              <AnimatedGroup className=\"space-y-8\" preset=\"blur-slide\">\n                {/* Badge with animated sparkle */}\n                <motion.div\n                  animate={\n                    shouldReduceMotion\n                      ? { opacity: 1 }\n                      : { opacity: 1, scale: 1, y: 0 }\n                  }\n                  initial={\n                    shouldReduceMotion\n                      ? { opacity: 1 }\n                      : { opacity: 0, scale: 0.9, y: 20 }\n                  }\n                  transition={\n                    shouldReduceMotion\n                      ? { duration: 0 }\n                      : {\n                          delay: 0.1,\n                          duration: 0.6,\n                          ease: [0.25, 0.46, 0.45, 0.94],\n                        }\n                  }\n                >\n                  <a\n                    className=\"mx-auto flex w-fit items-center justify-center gap-2 rounded-md py-0.5 pr-3 pl-1 transition-colors duration-200 ease-out hover:bg-foreground/5\"\n                    href=\"/\"\n                  >\n                    <svg\n                      aria-hidden=\"true\"\n                      className=\"h-4 w-auto\"\n                      fill=\"none\"\n                      viewBox=\"0 0 82 30\"\n                      width=\"200\"\n                      xmlns=\"http://www.w3.org/2000/svg\"\n                    >\n                      <path\n                        d=\"M23.81 14.013v.013l-1.075 4.665c-.058.264-.322.458-.626.458H20.81a.218.218 0 0 0-.208.155c-1.198 4.064-2.82 6.858-4.962 8.535-1.822 1.428-4.068 2.093-7.069 2.093-2.696 0-4.514-.867-6.056-2.578C.478 25.09-.364 21.388.146 16.926 1.065 8.549 5.41.096 13.776.096c2.545-.023 4.543.762 5.933 2.33 1.47 1.657 2.216 4.154 2.22 7.421a.55.55 0 0 1-.549.536h-6.13a.42.42 0 0 1-.407-.41c-.05-2.259-.72-3.36-2.052-3.36-2.35 0-3.736 3.19-4.471 4.959-1.027 2.47-1.55 5.152-1.447 7.824.049 1.244.249 2.994 1.43 3.718 1.047.643 2.541.217 3.446-.495.904-.711 1.632-1.942 1.938-3.065.043-.156.046-.277.005-.332-.043-.055-.162-.068-.253-.068h-1.574a.572.572 0 0 1-.438-.202.42.42 0 0 1-.087-.362l1.076-4.674c.053-.24.27-.42.537-.453v-.011h10.33c.024 0 .049 0 .072.005.268.034.457.284.452.556h.002Z\"\n                        fill=\"#0ae448\"\n                      />\n                      <path\n                        d=\"M41.594 8.65a.548.548 0 0 1-.548.531H35.4c-.37 0-.679-.3-.679-.665 0-1.648-.57-2.45-1.736-2.45s-1.918.717-1.94 1.968c-.025 1.395.764 2.662 3.01 4.84 2.957 2.774 4.142 5.232 4.085 8.48C38.047 26.605 34.476 30 29.042 30c-2.775 0-4.895-.743-6.305-2.207-1.431-1.486-2.087-3.668-1.95-6.485a.548.548 0 0 1 .549-.53h5.84a.55.55 0 0 1 .422.209.48.48 0 0 1 .106.384c-.065 1.016.112 1.775.512 2.195.256.272.613.41 1.058.41 1.079 0 1.711-.763 1.735-2.09.02-1.148-.343-2.155-2.321-4.19-2.555-2.496-4.846-5.075-4.775-9.13.042-2.351.976-4.502 2.631-6.056C28.294.868 30.687 0 33.465 0c2.783.02 4.892.813 6.269 2.359 1.304 1.466 1.932 3.582 1.862 6.29h-.002Z\"\n                        fill=\"#0ae448\"\n                      />\n                      <path\n                        d=\"m59.096 29.012.037-27.932a.525.525 0 0 0-.529-.533h-8.738c-.294 0-.423.252-.507.42L36.707 28.842v.005l-.005.006c-.14.343.126.71.497.71h6.108c.33 0 .548-.1.656-.308l1.213-2.915c.149-.388.177-.424.601-.424h5.836c.406 0 .415.008.408.405l-.131 2.71a.525.525 0 0 0 .529.532h6.17a.522.522 0 0 0 .403-.182.458.458 0 0 0 .104-.369Zm-10.81-9.326c-.057 0-.102-.001-.138-.005a.146.146 0 0 1-.13-.183c.012-.041.029-.095.053-.163l4.377-10.827c.038-.107.086-.212.136-.314.071-.145.157-.155.184-.047.023.09-.502 11.118-.502 11.118-.041.413-.06.43-.467.464l-3.509-.041h-.008l.003-.002Z\"\n                        fill=\"#0ae448\"\n                      />\n                      <path\n                        d=\"M71.545.547h-4.639c-.245 0-.52.13-.585.422l-6.455 28.029a.423.423 0 0 0 .088.364.572.572 0 0 0 .437.202h5.798c.311 0 .525-.153.583-.418 0 0 .703-3.168.704-3.178.05-.247-.036-.439-.258-.555-.105-.054-.209-.108-.312-.163l-1.005-.522-1-.522-.387-.201a.186.186 0 0 1-.102-.17.199.199 0 0 1 .198-.194l3.178.014c.95.005 1.901-.062 2.836-.234 6.58-1.215 10.95-6.485 11.076-13.656.107-6.12-3.309-9.221-10.15-9.221l-.005.003Zm-1.579 16.68h-.124c-.278 0-.328-.03-.337-.04-.004-.007 1.833-8.073 1.834-8.084.047-.233.045-.367-.099-.446-.184-.102-2.866-1.516-2.866-1.516a.188.188 0 0 1-.101-.172.197.197 0 0 1 .197-.192h4.241c1.32.04 2.056 1.221 2.021 3.237-.061 3.492-1.721 7.09-4.766 7.214Z\"\n                        fill=\"#0ae448\"\n                      />\n                    </svg>\n\n                    <span className=\"font-medium\">{badgeText}</span>\n                  </a>\n                </motion.div>\n\n                {/* Main heading with staggered animation */}\n                <AnimatedText\n                  as=\"h1\"\n                  className=\"mx-auto mt-8 max-w-3xl text-balance font-bold text-4xl tracking-tight sm:text-5xl\"\n                  delay={0.2}\n                >\n                  {heading}\n                </AnimatedText>\n\n                {/* Description */}\n                <AnimatedText\n                  as=\"p\"\n                  className=\"mx-auto my-6 max-w-xl text-balance text-muted-foreground text-xl\"\n                  delay={0.3}\n                >\n                  {description}\n                </AnimatedText>\n\n                {/* CTA buttons with staggered animation */}\n                <motion.div\n                  animate={\n                    shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }\n                  }\n                  className=\"flex items-center justify-center gap-3\"\n                  initial={\n                    shouldReduceMotion ? { opacity: 1 } : { opacity: 0, y: 20 }\n                  }\n                  transition={\n                    shouldReduceMotion\n                      ? { duration: 0 }\n                      : {\n                          delay: 0.4,\n                          duration: 0.6,\n                          ease: [0.25, 0.46, 0.45, 0.94],\n                        }\n                  }\n                >\n                  <motion.div\n                    transition={{\n                      damping: 10,\n                      stiffness: 400,\n                      type: \"spring\" as const,\n                    }}\n                    whileHover={shouldReduceMotion ? {} : { scale: 1.05 }}\n                    whileTap={shouldReduceMotion ? {} : { scale: 0.95 }}\n                  >\n                    <Button asChild size=\"lg\" variant=\"candy\">\n                      <a href={primaryButton.url}>\n                        <span className=\"text-nowrap\">\n                          {primaryButton.text}\n                        </span>\n                      </a>\n                    </Button>\n                  </motion.div>\n\n                  <motion.div\n                    transition={{\n                      damping: 10,\n                      stiffness: 400,\n                      type: \"spring\" as const,\n                    }}\n                    whileHover={shouldReduceMotion ? {} : { scale: 1.05 }}\n                    whileTap={shouldReduceMotion ? {} : { scale: 0.95 }}\n                  >\n                    <Button asChild size=\"lg\" variant=\"outline\">\n                      <a href={secondaryButton.url}>\n                        <span className=\"text-nowrap\">\n                          {secondaryButton.text}\n                        </span>\n                      </a>\n                    </Button>\n                  </motion.div>\n                </motion.div>\n              </AnimatedGroup>\n            </div>\n\n            {/* Image section with parallax-like animation */}\n            <motion.div\n              animate={{ opacity: 1, scale: 1, y: 0 }}\n              className=\"relative\"\n              initial={{ opacity: 0, scale: 0.95, y: 40 }}\n              transition={{\n                delay: 0.6,\n                duration: 0.8,\n                ease: [0.25, 0.46, 0.45, 0.94],\n              }}\n            >\n              <div className=\"relative z-10 mx-auto max-w-5xl px-6\">\n                <motion.div\n                  className=\"mt-12 md:mt-16\"\n                  transition={{\n                    damping: 20,\n                    stiffness: 300,\n                    type: \"spring\" as const,\n                  }}\n                  whileHover={\n                    shouldReduceMotion\n                      ? {}\n                      : {\n                          rotateY: 2,\n                          scale: 1.02,\n                        }\n                  }\n                >\n                  <motion.div\n                    className=\"relative mx-auto overflow-hidden rounded-(--radius) border border-transparent bg-background shadow-black/10 shadow-lg ring-1 ring-black/10\"\n                    initial={{\n                      boxShadow:\n                        \"0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 0 0 1px rgba(0, 0, 0, 0.1)\",\n                    }}\n                    transition={{\n                      duration: 0.3,\n                      ease: [0.25, 0.46, 0.45, 0.94],\n                    }}\n                    whileHover={\n                      shouldReduceMotion\n                        ? {}\n                        : {\n                            boxShadow:\n                              \"0 20px 40px -10px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(0, 0, 0, 0.1)\",\n                          }\n                    }\n                  >\n                    <motion.div\n                      animate={{ scale: 1 }}\n                      initial={\n                        shouldReduceMotion ? { scale: 1 } : { scale: 1.05 }\n                      }\n                      transition={\n                        shouldReduceMotion\n                          ? { duration: 0 }\n                          : {\n                              delay: 0.8,\n                              duration: 1.2,\n                              ease: [0.25, 0.46, 0.45, 0.94],\n                            }\n                      }\n                    >\n                      <img\n                        alt={imageAlt}\n                        className=\"h-auto w-full\"\n                        draggable={false}\n                        height=\"1842\"\n                        src={imageSrc}\n                        width=\"2880\"\n                      />\n                    </motion.div>\n                  </motion.div>\n                </motion.div>\n              </div>\n            </motion.div>\n          </div>\n        </motion.section>\n      </main>\n    </div>\n  );\n}\n\nexport default HeroProduct;\n","path":"index.tsx","target":"components/smoothui/header-2/index.tsx","type":"registry:block"}],"name":"header-2","registryDependencies":["https://smoothui.dev/r/shared.json","https://smoothui.dev/r/data.json"],"title":"Header 2","type":"registry:block"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["lucide-react","motion"],"description":"Premium header block with gradient logo and smooth animations","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { Avatar, AvatarImage } from \"@/components/ui/avatar\";\nimport { getAllPeople, getAvatarUrl, getImageKitUrl } from \"@/lib/smoothui-data\";\nimport { ArrowDownRight, Star } from \"lucide-react\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { AnimatedGroup, AnimatedText, Button, HeroHeader } from \"@/components/smoothui/shared\";\n\ninterface HeroShowcaseProps {\n  buttons?: {\n    primary?: {\n      text: string;\n      url: string;\n    };\n    secondary?: {\n      text: string;\n      url: string;\n    };\n  };\n  description?: string;\n  heading?: string;\n  reviews?: {\n    count: number;\n    avatars: {\n      src: string;\n      alt: string;\n    }[];\n    rating?: number;\n  };\n}\n\nexport function HeroShowcase({\n  heading = \"Build beautiful UIs, effortlessly.\",\n  description = \"Smoothui gives you the building blocks to create stunning, animated interfaces in minutes.\",\n  buttons = {\n    primary: {\n      text: \"Get Started\",\n      url: \"#link\",\n    },\n    secondary: {\n      text: \"Watch demo\",\n      url: \"#link\",\n    },\n  },\n  reviews = {\n    avatars: getAllPeople()\n      .slice(0, 5)\n      .map((person) => ({\n        alt: `${person.name} avatar`,\n        src: getAvatarUrl(person.avatar, 90),\n      })),\n    count: 200,\n    rating: 5.0,\n  },\n}: HeroShowcaseProps) {\n  const shouldReduceMotion = useReducedMotion();\n  return (\n    <>\n      <HeroHeader />\n      <main>\n        <motion.section\n          animate={\n            shouldReduceMotion\n              ? { opacity: 1 }\n              : { filter: \"blur(0px)\", opacity: 1, scale: 1 }\n          }\n          className=\"relative overflow-hidden bg-gradient-to-b from-background to-muted\"\n          initial={\n            shouldReduceMotion\n              ? { opacity: 1 }\n              : { filter: \"blur(12px)\", opacity: 0, scale: 1.04 }\n          }\n          transition={\n            shouldReduceMotion\n              ? { duration: 0 }\n              : { bounce: 0.32, duration: 0.9, type: \"spring\" as const }\n          }\n        >\n          <div className=\"mx-auto grid max-w-5xl items-center gap-10 px-6 py-24 lg:grid-cols-2 lg:gap-20\">\n            <AnimatedGroup\n              className=\"mx-auto flex flex-col items-center text-center md:ml-auto lg:max-w-3xl lg:items-start lg:text-left\"\n              preset=\"blur-slide\"\n            >\n              <AnimatedText\n                as=\"h1\"\n                className=\"my-6 text-pretty font-bold text-4xl lg:text-6xl xl:text-7xl\"\n              >\n                {heading}\n              </AnimatedText>\n              <AnimatedText\n                as=\"p\"\n                className=\"mb-8 max-w-xl text-foreground/70 lg:text-xl\"\n                delay={0.12}\n              >\n                {description}\n              </AnimatedText>\n              <AnimatedGroup\n                className=\"mb-12 flex w-fit flex-col items-center gap-4 sm:flex-row\"\n                preset=\"slide\"\n              >\n                <span className=\"inline-flex items-center -space-x-4\">\n                  {reviews.avatars.map((avatar, index) => (\n                    <motion.div\n                      key={`${avatar.src}-${index}`}\n                      style={{ display: \"inline-block\" }}\n                      transition={{\n                        damping: 20,\n                        stiffness: 300,\n                        type: \"spring\" as const,\n                      }}\n                      whileHover={shouldReduceMotion ? {} : { y: -8 }}\n                    >\n                      <Avatar className=\"size-12 border\">\n                        <AvatarImage alt={avatar.alt} src={avatar.src} />\n                      </Avatar>\n                    </motion.div>\n                  ))}\n                </span>\n                <div>\n                  <div className=\"flex items-center gap-1\">\n                    {[1, 2, 3, 4, 5].map((starNumber) => (\n                      <Star\n                        className=\"size-5 fill-yellow-400 text-yellow-400\"\n                        key={`star-${starNumber}`}\n                      />\n                    ))}\n                    <span className=\"mr-1 font-semibold\">\n                      {reviews.rating?.toFixed(1)}\n                    </span>\n                  </div>\n                  <p className=\"text-left font-medium text-foreground/70\">\n                    from {reviews.count}+ reviews\n                  </p>\n                </div>\n              </AnimatedGroup>\n              <AnimatedGroup\n                className=\"flex w-full flex-col justify-center gap-2 sm:flex-row lg:justify-start\"\n                preset=\"slide\"\n              >\n                {buttons.primary ? (\n                  <Button asChild className=\"w-full sm:w-auto\" variant=\"candy\">\n                    <a href={buttons.primary.url}>{buttons.primary.text}</a>\n                  </Button>\n                ) : null}\n                {buttons.secondary ? (\n                  <Button asChild variant=\"outline\">\n                    <a href={buttons.secondary.url}>\n                      {buttons.secondary.text}\n                      <ArrowDownRight className=\"size-4\" />\n                    </a>\n                  </Button>\n                ) : null}\n              </AnimatedGroup>\n            </AnimatedGroup>\n            {/* Imagen completamente estática para que el blend mode funcione perfecto */}\n            <div className=\"flex\">\n              <img\n                alt=\"app screen\"\n                className=\"h-full w-full rounded-md object-cover\"\n                draggable={false}\n                height={1842}\n                src={getImageKitUrl(\"/images/hero-example_xertaz.png\", {\n                  format: \"auto\",\n                  quality: 85,\n                  width: 1200,\n                })}\n                width={2880}\n              />\n            </div>\n          </div>\n        </motion.section>\n      </main>\n    </>\n  );\n}\nexport default HeroShowcase;\n","path":"index.tsx","target":"components/smoothui/header-3/index.tsx","type":"registry:block"}],"name":"header-3","registryDependencies":["avatar","https://smoothui.dev/r/shared.json","https://smoothui.dev/r/data.json"],"title":"Header 3","type":"registry:block"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["lucide-react","motion"],"description":"Interactive 3D grid hero header inspired by rauno.me 2024","devDependencies":[],"files":[{"content":"/**\n * Header 4 - Interactive 3D Grid Hero\n *\n * Inspired by the beautiful interactive grid design from rauno.me 2024 website.\n * Features a 3D-perspective grid with customizable hover colors that respond\n * to user interaction, creating an engaging and modern hero section.\n *\n * @see https://rauno.me\n */\n\n\"use client\";\n\nimport { ExternalLink } from \"lucide-react\";\nimport { useEffect, useRef } from \"react\";\nimport { AnimatedGroup, AnimatedText, Button, HeroHeader } from \"@/components/smoothui/shared\";\nimport styles from \"./hero-grid.module.css\";\n\ninterface InteractiveGridProps {\n  hoverColors?: string | [string, string, string, string];\n}\n\nconst TOTAL_TILES = 1600;\nconst INITIAL_TILE = 1;\nconst TILES_TO_CLONE = TOTAL_TILES - INITIAL_TILE;\n\nfunction InteractiveGrid({ hoverColors }: InteractiveGridProps) {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const tileRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    if (!(containerRef.current && tileRef.current)) {\n      return;\n    }\n\n    // Clone the first tile to create a 40x40 grid (1600 tiles total)\n    for (let i = 0; i < TILES_TO_CLONE; i++) {\n      const clonedTile = tileRef.current.cloneNode(true);\n      containerRef.current.appendChild(clonedTile);\n    }\n  }, []);\n\n  // Prepare CSS variables for hover colors\n  const style = hoverColors\n    ? {\n        \"--hero-grid-hover-color-1\": Array.isArray(hoverColors)\n          ? hoverColors[0]\n          : hoverColors,\n        \"--hero-grid-hover-color-2\": Array.isArray(hoverColors)\n          ? (hoverColors[1] ?? hoverColors[0])\n          : hoverColors,\n        \"--hero-grid-hover-color-3\": Array.isArray(hoverColors)\n          ? (hoverColors[2] ?? hoverColors[0])\n          : hoverColors,\n        \"--hero-grid-hover-color-4\": Array.isArray(hoverColors)\n          ? (hoverColors[3] ?? hoverColors[0])\n          : hoverColors,\n      }\n    : undefined;\n\n  return (\n    <div\n      aria-hidden=\"true\"\n      className={styles.gridContainer}\n      style={style as React.CSSProperties}\n    >\n      <div className={styles.mainGrid} ref={containerRef}>\n        <div className={styles.tile} ref={tileRef} />\n      </div>\n    </div>\n  );\n}\n\ninterface HeroGridProps {\n  hoverColors?: string | [string, string, string, string];\n}\n\nexport function HeroGrid({ hoverColors }: HeroGridProps) {\n  return (\n    <div className=\"relative\">\n      <HeroHeader />\n      <main>\n        <section className=\"relative overflow-hidden py-36\">\n          {/* Interactive animated grid background */}\n          <InteractiveGrid hoverColors={hoverColors} />\n          <AnimatedGroup\n            className=\"pointer-events-none flex flex-col items-center gap-6 text-center\"\n            preset=\"blur-slide\"\n          >\n            <div>\n              <AnimatedText\n                as=\"h1\"\n                className=\"mb-6 text-pretty font-bold text-2xl tracking-tight lg:text-5xl\"\n              >\n                Build your next project with{\" \"}\n                <span className=\"text-brand\">Smoothui</span>\n              </AnimatedText>\n              <AnimatedText\n                as=\"p\"\n                className=\"mx-auto max-w-3xl text-muted-foreground lg:text-xl\"\n                delay={0.15}\n              >\n                Smoothui gives you the building blocks to create stunning,\n                animated interfaces in minutes.\n              </AnimatedText>\n            </div>\n            <AnimatedGroup\n              className=\"pointer-events-auto mt-6 flex justify-center gap-3\"\n              preset=\"slide\"\n            >\n              <Button\n                className=\"shadow-sm transition-shadow hover:shadow\"\n                variant=\"outline\"\n              >\n                Get Started\n              </Button>\n              <Button className=\"group\" variant=\"candy\">\n                Learn more{\" \"}\n                <ExternalLink className=\"ml-2 h-4 transition-transform group-hover:translate-x-0.5\" />\n              </Button>\n            </AnimatedGroup>\n          </AnimatedGroup>\n        </section>\n      </main>\n    </div>\n  );\n}\n\nexport default HeroGrid;\n","path":"index.tsx","target":"components/smoothui/header-4/index.tsx","type":"registry:block"},{"content":"/* @reference \"../../../../app/styles/smoothui.css\"; */\n\n/**\n * Interactive 3D Grid Styles\n *\n * Inspired by rauno.me 2024 website's interactive grid design.\n * Creates a 3D-perspective grid with customizable hover colors.\n */\n\n.gridContainer {\n  --hero-grid-hover-color-1: var(--color-brand);\n  --hero-grid-hover-color-2: var(--color-brand);\n  --hero-grid-hover-color-3: var(--color-brand);\n  --hero-grid-hover-color-4: var(--color-brand);\n  position: absolute;\n  top: 0;\n  left: 0;\n  width: 100%;\n  height: 100%;\n  overflow: hidden;\n  perspective: 2000px;\n  perspective-origin: center center;\n}\n\n.mainGrid {\n  position: absolute;\n  top: 50%;\n  left: 50%;\n  display: grid;\n  grid-template-rows: repeat(40, 1fr);\n  grid-template-columns: repeat(40, 1fr);\n  width: 140rem;\n  aspect-ratio: 1;\n  transform: translate(-50%, -50%) rotateX(50deg) rotateY(-5deg) rotateZ(20deg)\n    scale(1.25);\n}\n\n.mainGrid::after {\n  position: absolute;\n  inset: 0;\n  z-index: 3;\n  pointer-events: none;\n  content: \"\";\n  background: radial-gradient(circle, transparent 25%, rgb(255 255 255) 80%);\n}\n\n:global(.dark).mainGrid::after {\n  background: radial-gradient(\n    circle,\n    transparent 25%,\n    rgb(0 0 0 / 90%) 80%\n  ) !important;\n}\n\n.tile {\n  border: 0.5px solid rgb(0 0 0 / 12%);\n  transition: background-color 1500ms;\n}\n\n:global(.dark).tile {\n  border: 0.5px solid rgb(255 255 255 / 5%);\n}\n\n.tile:hover {\n  transition-duration: 0ms;\n}\n\n.tile:nth-child(4n):hover {\n  background-color: var(--hero-grid-hover-color-1);\n}\n\n.tile:nth-child(4n + 1):hover {\n  background-color: var(--hero-grid-hover-color-2);\n}\n\n.tile:nth-child(4n + 2):hover {\n  background-color: var(--hero-grid-hover-color-3);\n}\n\n.tile:nth-child(4n + 3):hover {\n  background-color: var(--hero-grid-hover-color-4);\n}\n\n.tile:nth-child(7n):hover {\n  background-color: var(--hero-grid-hover-color-2);\n}\n\n.tile:nth-child(7n + 3):hover {\n  background-color: var(--hero-grid-hover-color-3);\n}\n\n.tile:nth-child(7n + 5):hover {\n  background-color: var(--hero-grid-hover-color-4);\n}\n\n.tile:nth-child(7n + 6):hover {\n  background-color: var(--hero-grid-hover-color-1);\n}\n\n.tile:nth-child(11n + 1):hover {\n  background-color: var(--hero-grid-hover-color-1);\n}\n\n.tile:nth-child(11n + 4):hover {\n  background-color: var(--hero-grid-hover-color-2);\n}\n\n.tile:nth-child(11n + 7):hover {\n  background-color: var(--hero-grid-hover-color-3);\n}\n\n.tile:nth-child(11n + 10):hover {\n  background-color: var(--hero-grid-hover-color-4);\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .mainGrid {\n    transform: translate(-50%, -50%) rotateZ(20deg);\n  }\n\n  .tile {\n    transition: none;\n  }\n}\n\n/* Reduce overlay size of hero text/button containers */\n.heroContent {\n  display: flex;\n  flex-direction: column;\n  align-items: flex-start;\n  max-width: 600px;\n  padding: 0;\n  margin: 0 auto;\n  background: none;\n  box-shadow: none;\n}\n\n.heroButtons {\n  display: flex;\n  gap: 1rem;\n  width: auto;\n  padding: 0;\n  margin-top: 1.5rem;\n  background: none;\n  box-shadow: none;\n}\n","path":"hero-grid.module.css","target":"components/smoothui/header-4/hero-grid.module.css","type":"registry:block"}],"name":"header-4","registryDependencies":["https://smoothui.dev/r/shared.json","https://smoothui.dev/r/tokens.json"],"title":"Header 4","type":"registry:block"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"Spotlight hero block with dark theme and gradient text","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport SmoothButton from \"@/components/smoothui/smooth-button\";\nimport { motion, useReducedMotion } from \"motion/react\";\n\nconst SPRING = {\n  bounce: 0.1,\n  duration: 0.25,\n  type: \"spring\" as const,\n};\n\nexport function HeroSpotlight() {\n  const shouldReduceMotion = useReducedMotion();\n\n  return (\n    <section aria-labelledby=\"hero-spotlight-heading\">\n      <div className=\"relative flex min-h-[600px] items-center justify-center overflow-hidden bg-zinc-950 py-24 md:py-32\">\n        {/* Spotlight beam */}\n        <motion.div\n          aria-hidden=\"true\"\n          className=\"pointer-events-none absolute top-0 left-1/2 h-full w-[600px] -translate-x-1/2\"\n          initial={\n            shouldReduceMotion ? { opacity: 0.15 } : { opacity: 0, scaleX: 0 }\n          }\n          style={{\n            background:\n              \"conic-gradient(from 180deg at 50% 0%, transparent 40%, rgba(120, 119, 198, 0.12) 50%, transparent 60%)\",\n          }}\n          transition={\n            shouldReduceMotion\n              ? { duration: 0 }\n              : { duration: 0.8, ease: [0.23, 1, 0.32, 1] }\n          }\n          viewport={{ once: true }}\n          whileInView={\n            shouldReduceMotion\n              ? { opacity: 0.15 }\n              : { opacity: 0.15, scaleX: 1 }\n          }\n        />\n\n        {/* Floating particles */}\n        <div\n          aria-hidden=\"true\"\n          className=\"pointer-events-none absolute inset-0\"\n        >\n          {Array.from({ length: 20 }, (_, i) => (\n            <div\n              className=\"absolute h-1 w-1 rounded-full bg-white/20\"\n              key={`particle-${i}`}\n              style={{\n                animation: `pulse ${2 + (i % 3)}s ease-in-out infinite ${(i % 5) * 0.5}s`,\n                left: `${(i * 37 + 13) % 100}%`,\n                top: `${(i * 53 + 7) % 100}%`,\n              }}\n            />\n          ))}\n        </div>\n\n        <div className=\"relative z-10 mx-auto max-w-4xl px-6 text-center\">\n          <motion.div\n            initial={\n              shouldReduceMotion ? { opacity: 1 } : { opacity: 0, y: 24 }\n            }\n            transition={\n              shouldReduceMotion\n                ? { duration: 0 }\n                : { ...SPRING, staggerChildren: 0.08 }\n            }\n            viewport={{ once: true }}\n            whileInView={\n              shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }\n            }\n          >\n            <h1\n              className=\"bg-gradient-to-b from-white to-zinc-400 bg-clip-text font-bold text-4xl text-transparent tracking-tight md:text-6xl lg:text-7xl\"\n              id=\"hero-spotlight-heading\"\n            >\n              Build stunning interfaces\n            </h1>\n            <p className=\"mx-auto mt-6 max-w-xl text-lg text-zinc-400\">\n              Beautifully animated components built with React, Motion, and\n              Tailwind CSS. Open source and ready for production.\n            </p>\n            <div className=\"mt-10 flex flex-wrap items-center justify-center gap-4\">\n              <SmoothButton\n                className=\"bg-white text-zinc-900 hover:bg-zinc-200\"\n                size=\"lg\"\n              >\n                Get Started\n              </SmoothButton>\n              <SmoothButton\n                className=\"border-zinc-700 bg-transparent text-zinc-300 hover:bg-zinc-800\"\n                size=\"lg\"\n                variant=\"outline\"\n              >\n                Documentation\n              </SmoothButton>\n            </div>\n          </motion.div>\n        </div>\n\n        <style>{`\n          @keyframes pulse {\n            0%, 100% { opacity: 0.2; }\n            50% { opacity: 0.8; }\n          }\n        `}</style>\n      </div>\n    </section>\n  );\n}\n\nexport default HeroSpotlight;\n","path":"index.tsx","target":"components/smoothui/header-5/index.tsx","type":"registry:block"}],"name":"header-5","registryDependencies":["https://smoothui.dev/r/smooth-button.json"],"title":"Header 5","type":"registry:block"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"Minimal hero block with clean typography","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { motion, useReducedMotion } from \"motion/react\";\n\nexport function HeroMinimal() {\n  const shouldReduceMotion = useReducedMotion();\n\n  return (\n    <section aria-labelledby=\"hero-minimal-heading\">\n      <div className=\"flex min-h-[500px] items-center justify-center py-24 md:py-32\">\n        <motion.div\n          className=\"mx-auto max-w-2xl px-6 text-center\"\n          initial={shouldReduceMotion ? { opacity: 1 } : { opacity: 0 }}\n          transition={\n            shouldReduceMotion\n              ? { duration: 0 }\n              : { duration: 0.4, ease: [0.23, 1, 0.32, 1] }\n          }\n          viewport={{ once: true }}\n          whileInView={shouldReduceMotion ? { opacity: 1 } : { opacity: 1 }}\n        >\n          <motion.h1\n            className=\"font-bold text-4xl tracking-tight md:text-5xl lg:text-6xl\"\n            id=\"hero-minimal-heading\"\n            initial={\n              shouldReduceMotion ? undefined : { letterSpacing: \"0.05em\" }\n            }\n            transition={\n              shouldReduceMotion\n                ? { duration: 0 }\n                : { duration: 0.6, ease: [0.23, 1, 0.32, 1] }\n            }\n            viewport={{ once: true }}\n            whileInView={\n              shouldReduceMotion ? undefined : { letterSpacing: \"0em\" }\n            }\n          >\n            Less is more\n          </motion.h1>\n          <p className=\"mt-6 text-foreground/60 text-lg\">\n            Simple, elegant components that speak for themselves.\n          </p>\n          <a\n            className={cn(\n              \"mt-10 inline-flex items-center gap-1 font-medium text-foreground text-sm underline underline-offset-4 transition-colors hover:text-foreground/70\",\n              \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\"\n            )}\n            href=\"#\"\n          >\n            Explore components →\n          </a>\n        </motion.div>\n      </div>\n    </section>\n  );\n}\n\nexport default HeroMinimal;\n","path":"index.tsx","target":"components/smoothui/header-6/index.tsx","type":"registry:block"}],"name":"header-6","registryDependencies":[],"title":"Header 6","type":"registry:block"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":[],"description":"Simple logo cloud block with grid layout","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport {\n  Canpoy,\n  Canva,\n  Casetext,\n  Clearbit,\n  Descript,\n  Duolingo,\n  Faire,\n  IDEO,\n  KhanAcademy,\n  Quizlet,\n  Ramp,\n  Strava,\n} from \"@/components/smoothui/shared\";\n\nconst LOGOS = [\n  <Canpoy key=\"1\" />,\n  <Canva key=\"2\" />,\n  <Casetext key=\"3\" />,\n  <Strava key=\"4\" />,\n  <Descript key=\"5\" />,\n  <Duolingo key=\"6\" />,\n  <Faire key=\"7\" />,\n  <Clearbit key=\"8\" />,\n  <IDEO key=\"9\" />,\n  <KhanAcademy key=\"10\" />,\n  <Quizlet key=\"11\" />,\n  <Ramp key=\"12\" />,\n];\n\ninterface LogoCloudSimpleProps {\n  count?: number;\n  description?: string;\n  title?: string;\n}\n\nexport function LogoCloudSimple({\n  title = \"You're in good company\",\n  description = \"Trusted by leading teams from around the world\",\n  count = 4,\n}: LogoCloudSimpleProps) {\n  const logos = LOGOS.slice(0, count);\n\n  return (\n    <section className=\"bg-background py-16\">\n      <div className=\"mx-auto max-w-5xl px-6\">\n        <div className=\"mx-auto mb-12 max-w-xl text-balance text-center md:mb-16\">\n          <h2 className=\"font-semibold text-4xl\">{title}</h2>\n          <p className=\"mt-4 text-lg text-muted-foreground\">{description}</p>\n        </div>\n        <div className=\"mx-auto grid max-w-5xl grid-cols-2 items-center gap-8 md:grid-cols-4\">\n          {logos.map((logo) => (\n            <div\n              className=\"flex items-center justify-center opacity-60 transition-opacity duration-200 *:fill-foreground hover:opacity-100\"\n              key={logo.key}\n            >\n              {logo}\n            </div>\n          ))}\n        </div>\n      </div>\n    </section>\n  );\n}\n\nexport default LogoCloudSimple;\n","path":"index.tsx","target":"components/smoothui/logo-cloud-1/index.tsx","type":"registry:block"}],"name":"logo-cloud-1","registryDependencies":["https://smoothui.dev/r/shared.json"],"title":"Logo Cloud 1","type":"registry:block"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"Animated logo cloud block with infinite scroll","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { motion, useReducedMotion } from \"motion/react\";\nimport type React from \"react\";\nimport {\n  Canpoy,\n  Canva,\n  Casetext,\n  Clearbit,\n  Descript,\n  Duolingo,\n  Faire,\n  Strava,\n} from \"@/components/smoothui/shared\";\n\nconst ANIMATION_DURATION = 25;\nconst STAGGER_DELAY = 0.1;\nconst HOVER_SCALE = 1.2;\nconst HOVER_ROTATE = 5;\nconst SPRING_STIFFNESS = 300;\nconst SCROLL_DISTANCE_FACTOR = 33.333;\n\ninterface LogoCloudAnimatedProps {\n  description?: string;\n  logos?: Array<{\n    name: string;\n    logo: React.ComponentType;\n    url?: string;\n  }>;\n  title?: string;\n}\n\nexport function LogoCloudAnimated({\n  title = \"Trusted by the world's most innovative teams\",\n  description = \"Join thousands of developers and designers who are already building with Smoothui\",\n  logos = [\n    { logo: Canpoy, name: \"Canpoy\", url: \"https://canpoy.com\" },\n    { logo: Canva, name: \"Canva\", url: \"https://canva.com\" },\n    { logo: Casetext, name: \"Casetext\", url: \"https://casetext.com\" },\n    { logo: Strava, name: \"Strava\", url: \"https://strava.com\" },\n    { logo: Descript, name: \"Descript\", url: \"https://descript.com\" },\n    { logo: Duolingo, name: \"Duolingo\", url: \"https://duolingo.com\" },\n    { logo: Faire, name: \"Faire\", url: \"https://faire.com\" },\n    { logo: Clearbit, name: \"Clearbit\", url: \"https://clearbit.com\" },\n  ],\n}: LogoCloudAnimatedProps) {\n  const shouldReduceMotion = useReducedMotion();\n  return (\n    <section className=\"overflow-hidden py-20\">\n      <div className=\"mx-auto max-w-7xl px-6\">\n        <motion.div\n          animate={shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }}\n          className=\"mb-16 text-center\"\n          initial={shouldReduceMotion ? { opacity: 1 } : { opacity: 0, y: 20 }}\n          transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.6 }}\n        >\n          <h2 className=\"mb-4 font-bold text-2xl text-foreground lg:text-3xl\">\n            {title}\n          </h2>\n          <p className=\"text-foreground/70 text-lg\">{description}</p>\n        </motion.div>\n        {/* Infinite scrolling logos */}\n        <div\n          className=\"relative overflow-hidden\"\n          style={{\n            maskImage:\n              \"linear-gradient(to right, hsl(0 0% 0% / 0), hsl(0 0% 0% / 1) 20%, hsl(0 0% 0% / 1) 80%, hsl(0 0% 0% / 0))\",\n            WebkitMaskImage:\n              \"linear-gradient(to right, hsl(0 0% 0% / 0), hsl(0 0% 0% / 1) 20%, hsl(0 0% 0% / 1) 80%, hsl(0 0% 0% / 0))\",\n          }}\n        >\n          <motion.div\n            animate={\n              shouldReduceMotion\n                ? { x: 0 }\n                : { x: [0, -SCROLL_DISTANCE_FACTOR * logos.length] }\n            }\n            className=\"flex min-w-full shrink-0 items-center justify-around gap-8\"\n            transition={\n              shouldReduceMotion\n                ? { duration: 0 }\n                : {\n                    x: {\n                      duration: ANIMATION_DURATION,\n                      ease: \"linear\",\n                      repeat: Number.POSITIVE_INFINITY,\n                      repeatType: \"loop\",\n                    },\n                  }\n            }\n          >\n            {/* First set */}\n            {logos.map((logo, index) => (\n              <motion.a\n                animate={\n                  shouldReduceMotion ? { opacity: 1 } : { opacity: 1, scale: 1 }\n                }\n                aria-label={`Visit ${logo.name}`}\n                className=\"group flex shrink-0 flex-col items-center justify-center p-6 transition-all hover:scale-105\"\n                href={logo.url}\n                initial={\n                  shouldReduceMotion\n                    ? { opacity: 1 }\n                    : { opacity: 0, scale: 0.8 }\n                }\n                key={`first-${logo.name}`}\n                rel=\"noopener noreferrer\"\n                target=\"_blank\"\n                transition={\n                  shouldReduceMotion\n                    ? { duration: 0 }\n                    : {\n                        delay: index * STAGGER_DELAY,\n                        duration: 0.4,\n                      }\n                }\n              >\n                <motion.div\n                  className=\"mb-2 text-4xl *:fill-foreground\"\n                  transition={\n                    shouldReduceMotion\n                      ? { duration: 0 }\n                      : { stiffness: SPRING_STIFFNESS, type: \"spring\" as const }\n                  }\n                  whileHover={\n                    shouldReduceMotion\n                      ? {}\n                      : { rotate: HOVER_ROTATE, scale: HOVER_SCALE }\n                  }\n                >\n                  <logo.logo />\n                </motion.div>\n              </motion.a>\n            ))}\n            {/* Second set for seamless loop */}\n            {logos.map((logo, index) => (\n              <motion.a\n                animate={\n                  shouldReduceMotion ? { opacity: 1 } : { opacity: 1, scale: 1 }\n                }\n                aria-label={`Visit ${logo.name}`}\n                className=\"group flex shrink-0 flex-col items-center justify-center p-6 transition-all hover:scale-105\"\n                href={logo.url}\n                initial={\n                  shouldReduceMotion\n                    ? { opacity: 1 }\n                    : { opacity: 0, scale: 0.8 }\n                }\n                key={`second-${logo.name}`}\n                rel=\"noopener noreferrer\"\n                target=\"_blank\"\n                transition={\n                  shouldReduceMotion\n                    ? { duration: 0 }\n                    : { delay: index * STAGGER_DELAY, duration: 0.4 }\n                }\n              >\n                <motion.div\n                  className=\"mb-2 text-4xl *:fill-foreground\"\n                  transition={\n                    shouldReduceMotion\n                      ? { duration: 0 }\n                      : { stiffness: SPRING_STIFFNESS, type: \"spring\" as const }\n                  }\n                  whileHover={\n                    shouldReduceMotion\n                      ? {}\n                      : { rotate: HOVER_ROTATE, scale: HOVER_SCALE }\n                  }\n                >\n                  <logo.logo />\n                </motion.div>\n              </motion.a>\n            ))}\n            {/* Third set for even smoother loop */}\n            {logos.map((logo, index) => (\n              <motion.a\n                animate={\n                  shouldReduceMotion ? { opacity: 1 } : { opacity: 1, scale: 1 }\n                }\n                aria-label={`Visit ${logo.name}`}\n                className=\"group flex shrink-0 flex-col items-center justify-center p-6 transition-all hover:scale-105\"\n                href={logo.url}\n                initial={\n                  shouldReduceMotion\n                    ? { opacity: 1 }\n                    : { opacity: 0, scale: 0.8 }\n                }\n                key={`third-${logo.name}`}\n                rel=\"noopener noreferrer\"\n                target=\"_blank\"\n                transition={\n                  shouldReduceMotion\n                    ? { duration: 0 }\n                    : { delay: index * STAGGER_DELAY, duration: 0.4 }\n                }\n              >\n                <motion.div\n                  className=\"mb-2 text-4xl *:fill-foreground\"\n                  transition={\n                    shouldReduceMotion\n                      ? { duration: 0 }\n                      : { stiffness: SPRING_STIFFNESS, type: \"spring\" as const }\n                  }\n                  whileHover={\n                    shouldReduceMotion\n                      ? {}\n                      : { rotate: HOVER_ROTATE, scale: HOVER_SCALE }\n                  }\n                >\n                  <logo.logo />\n                </motion.div>\n              </motion.a>\n            ))}\n          </motion.div>\n        </div>\n      </div>\n    </section>\n  );\n}\n\nexport default LogoCloudAnimated;\n","path":"index.tsx","target":"components/smoothui/logo-cloud-2/index.tsx","type":"registry:block"}],"name":"logo-cloud-2","registryDependencies":["https://smoothui.dev/r/shared.json"],"title":"Logo Cloud 2","type":"registry:block"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":[],"description":"Infinite scrolling logo marquee","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport type React from \"react\";\nimport {\n  Canpoy,\n  Canva,\n  Casetext,\n  Clearbit,\n  Descript,\n  Duolingo,\n  Faire,\n  Strava,\n} from \"@/components/smoothui/shared\";\n\nexport interface LogoMarqueeProps {\n  description?: string;\n  direction?: \"left\" | \"right\";\n  logos?: Array<{\n    name: string;\n    logo: React.ReactNode;\n  }>;\n  pauseOnHover?: boolean;\n  speed?: \"slow\" | \"normal\" | \"fast\";\n  title?: string;\n}\n\nconst DEFAULT_LOGOS = [\n  { logo: <Canpoy />, name: \"Canpoy\" },\n  { logo: <Canva />, name: \"Canva\" },\n  { logo: <Casetext />, name: \"Casetext\" },\n  { logo: <Strava />, name: \"Strava\" },\n  { logo: <Descript />, name: \"Descript\" },\n  { logo: <Duolingo />, name: \"Duolingo\" },\n  { logo: <Faire />, name: \"Faire\" },\n  { logo: <Clearbit />, name: \"Clearbit\" },\n];\n\nconst SPEED_MAP = {\n  fast: \"20s\",\n  normal: \"40s\",\n  slow: \"60s\",\n};\n\nexport function LogoMarquee({\n  title = \"Trusted by industry leaders\",\n  description = \"Join thousands of companies already using our platform\",\n  logos = DEFAULT_LOGOS,\n  speed = \"normal\",\n  direction = \"left\",\n  pauseOnHover = true,\n}: LogoMarqueeProps) {\n  const animationDuration = SPEED_MAP[speed];\n  const animationDirection = direction === \"right\" ? \"reverse\" : \"normal\";\n\n  return (\n    <section className=\"overflow-hidden py-20\">\n      <style>\n        {`\n          @keyframes marquee-scroll {\n            from {\n              transform: translateX(0);\n            }\n            to {\n              transform: translateX(-50%);\n            }\n          }\n\n          .marquee-track {\n            animation: marquee-scroll var(--marquee-duration, 40s) linear infinite;\n            animation-direction: var(--marquee-direction, normal);\n          }\n\n          .marquee-container:hover .marquee-track {\n            animation-play-state: var(--marquee-pause-on-hover, running);\n          }\n\n          @media (prefers-reduced-motion: reduce) {\n            .marquee-track {\n              animation: none;\n            }\n          }\n        `}\n      </style>\n      <div className=\"mx-auto max-w-7xl px-6\">\n        <div className=\"mb-12 text-center\">\n          <h2 className=\"mb-4 font-bold text-2xl text-foreground lg:text-3xl\">\n            {title}\n          </h2>\n          <p className=\"text-foreground/70 text-lg\">{description}</p>\n        </div>\n\n        <div\n          className=\"marquee-container relative overflow-hidden\"\n          style={{\n            maskImage:\n              \"linear-gradient(to right, transparent, black 10%, black 90%, transparent)\",\n            WebkitMaskImage:\n              \"linear-gradient(to right, transparent, black 10%, black 90%, transparent)\",\n          }}\n        >\n          <div\n            className=\"marquee-track flex w-max\"\n            style={\n              {\n                \"--marquee-direction\": animationDirection,\n                \"--marquee-duration\": animationDuration,\n                \"--marquee-pause-on-hover\": pauseOnHover ? \"paused\" : \"running\",\n              } as React.CSSProperties\n            }\n          >\n            {/* First set of logos */}\n            {logos.map((logo, index) => (\n              <div\n                className=\"flex shrink-0 items-center justify-center px-8 py-4 opacity-60 transition-opacity duration-200 *:fill-foreground hover:opacity-100\"\n                key={`first-${logo.name}-${index}`}\n              >\n                {logo.logo}\n              </div>\n            ))}\n            {/* Second set of logos for seamless loop */}\n            {logos.map((logo, index) => (\n              <div\n                className=\"flex shrink-0 items-center justify-center px-8 py-4 opacity-60 transition-opacity duration-200 *:fill-foreground hover:opacity-100\"\n                key={`second-${logo.name}-${index}`}\n              >\n                {logo.logo}\n              </div>\n            ))}\n          </div>\n        </div>\n      </div>\n    </section>\n  );\n}\n\nexport default LogoMarquee;\n","path":"index.tsx","target":"components/smoothui/logo-cloud-3/index.tsx","type":"registry:block"}],"name":"logo-cloud-3","registryDependencies":["https://smoothui.dev/r/shared.json"],"title":"Logo Cloud 3","type":"registry:block"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"Interactive logo grid with hover effects","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport type React from \"react\";\nimport { useEffect, useState } from \"react\";\nimport {\n  Canpoy,\n  Canva,\n  Casetext,\n  Clearbit,\n  Descript,\n  Duolingo,\n  Faire,\n  Strava,\n} from \"@/components/smoothui/shared\";\n\nexport interface LogoGridProps {\n  columns?: 3 | 4 | 5 | 6;\n  description?: string;\n  logos?: Array<{\n    name: string;\n    logo: React.ReactNode;\n    href?: string;\n  }>;\n  title?: string;\n}\n\nconst DEFAULT_LOGOS = [\n  { logo: <Canpoy />, name: \"Canpoy\" },\n  { logo: <Canva />, name: \"Canva\" },\n  { logo: <Casetext />, name: \"Casetext\" },\n  { logo: <Strava />, name: \"Strava\" },\n  { logo: <Descript />, name: \"Descript\" },\n  { logo: <Duolingo />, name: \"Duolingo\" },\n  { logo: <Faire />, name: \"Faire\" },\n  { logo: <Clearbit />, name: \"Clearbit\" },\n];\n\nconst COLUMN_CLASSES = {\n  3: \"grid-cols-2 sm:grid-cols-3\",\n  4: \"grid-cols-2 sm:grid-cols-4\",\n  5: \"grid-cols-2 sm:grid-cols-3 lg:grid-cols-5\",\n  6: \"grid-cols-2 sm:grid-cols-3 lg:grid-cols-6\",\n};\n\n// Animation constants\nconst HOVER_LIFT_OFFSET = -4;\nconst INACTIVE_OPACITY = 0.6;\n\ninterface LogoItemProps {\n  isHoverDevice: boolean;\n  logo: {\n    name: string;\n    logo: React.ReactNode;\n    href?: string;\n  };\n  shouldReduceMotion: boolean | null;\n}\n\nfunction LogoItem({ logo, isHoverDevice, shouldReduceMotion }: LogoItemProps) {\n  const [isHovered, setIsHovered] = useState(false);\n\n  const content = (\n    <motion.div\n      animate={\n        shouldReduceMotion\n          ? {}\n          : {\n              y: isHovered ? HOVER_LIFT_OFFSET : 0,\n            }\n      }\n      className=\"group relative flex items-center justify-center p-6\"\n      onMouseEnter={() => isHoverDevice && setIsHovered(true)}\n      onMouseLeave={() => isHoverDevice && setIsHovered(false)}\n      transition={\n        shouldReduceMotion\n          ? { duration: 0 }\n          : {\n              bounce: 0.05,\n              duration: 0.25,\n              type: \"spring\" as const,\n            }\n      }\n    >\n      {/* Logo with grayscale effect */}\n      <div\n        className=\"transition-all duration-200 *:fill-foreground\"\n        style={{\n          filter: isHovered ? \"grayscale(0)\" : \"grayscale(1)\",\n          opacity: isHovered ? 1 : INACTIVE_OPACITY,\n        }}\n      >\n        {logo.logo}\n      </div>\n\n      {/* Tooltip */}\n      <AnimatePresence>\n        {isHovered ? (\n          <motion.div\n            animate={{ opacity: 1, scale: 1, x: \"-50%\", y: 0 }}\n            className=\"pointer-events-none absolute -top-2 left-1/2 z-10 whitespace-nowrap rounded-md bg-foreground px-3 py-1.5 font-medium text-background text-sm shadow-lg\"\n            exit={\n              shouldReduceMotion\n                ? { opacity: 0, transition: { duration: 0 } }\n                : { opacity: 0, scale: 0.95, y: 4 }\n            }\n            initial={\n              shouldReduceMotion\n                ? { opacity: 1, x: \"-50%\", y: 0 }\n                : { opacity: 0, scale: 0.95, x: \"-50%\", y: 4 }\n            }\n            transition={\n              shouldReduceMotion\n                ? { duration: 0 }\n                : {\n                    bounce: 0.05,\n                    duration: 0.25,\n                    type: \"spring\" as const,\n                  }\n            }\n          >\n            {logo.name}\n            {/* Tooltip arrow */}\n            <div className=\"absolute -bottom-1 left-1/2 h-2 w-2 -translate-x-1/2 rotate-45 bg-foreground\" />\n          </motion.div>\n        ) : null}\n      </AnimatePresence>\n    </motion.div>\n  );\n\n  if (logo.href) {\n    return (\n      <a\n        className=\"focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\"\n        href={logo.href}\n        rel=\"noopener noreferrer\"\n        target=\"_blank\"\n      >\n        {content}\n      </a>\n    );\n  }\n\n  return content;\n}\n\nexport function LogoGrid({\n  title = \"Trusted by innovative companies\",\n  description = \"Leading organizations rely on our platform to power their success\",\n  logos = DEFAULT_LOGOS,\n  columns = 4,\n}: LogoGridProps) {\n  const shouldReduceMotion = useReducedMotion();\n  const [isHoverDevice, setIsHoverDevice] = useState(false);\n\n  useEffect(() => {\n    const mediaQuery = window.matchMedia(\"(hover: hover) and (pointer: fine)\");\n    setIsHoverDevice(mediaQuery.matches);\n\n    const handleChange = (e: MediaQueryListEvent) => {\n      setIsHoverDevice(e.matches);\n    };\n\n    mediaQuery.addEventListener(\"change\", handleChange);\n    return () => mediaQuery.removeEventListener(\"change\", handleChange);\n  }, []);\n\n  return (\n    <section className=\"bg-background py-20\">\n      <div className=\"mx-auto max-w-6xl px-6\">\n        <div className=\"mb-12 text-center\">\n          <h2 className=\"mb-4 font-bold text-2xl text-foreground lg:text-3xl\">\n            {title}\n          </h2>\n          <p className=\"text-foreground/70 text-lg\">{description}</p>\n        </div>\n\n        <div\n          className={`grid ${COLUMN_CLASSES[columns]} gap-4 rounded-xl border border-border/50 bg-muted/30 p-4`}\n        >\n          {logos.map((logo, index) => (\n            <LogoItem\n              isHoverDevice={isHoverDevice}\n              key={`${logo.name}-${index}`}\n              logo={logo}\n              shouldReduceMotion={shouldReduceMotion}\n            />\n          ))}\n        </div>\n      </div>\n    </section>\n  );\n}\n\nexport default LogoGrid;\n","path":"index.tsx","target":"components/smoothui/logo-cloud-4/index.tsx","type":"registry:block"}],"name":"logo-cloud-4","registryDependencies":["https://smoothui.dev/r/shared.json"],"title":"Logo Cloud 4","type":"registry:block"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"Simple pricing block with single plan","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport SmoothButton from \"@/components/smoothui/smooth-button\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { useEffect, useRef, useState } from \"react\";\n\ninterface PriceFlowProps {\n  className?: string;\n  value: number;\n}\n\nfunction PriceFlow({ value, className = \"\" }: PriceFlowProps) {\n  const [prevValue, setPrevValue] = useState(value);\n  const prevTensRef = useRef<HTMLSpanElement>(null);\n  const nextTensRef = useRef<HTMLSpanElement>(null);\n  const prevOnesRef = useRef<HTMLSpanElement>(null);\n  const nextOnesRef = useRef<HTMLSpanElement>(null);\n\n  useEffect(() => {\n    if (value !== prevValue) {\n      const prevTens = prevTensRef.current;\n      const nextTens = nextTensRef.current;\n      const prevOnes = prevOnesRef.current;\n      const nextOnes = nextOnesRef.current;\n\n      if (\n        prevTens &&\n        nextTens &&\n        Math.floor(value / 10) !== Math.floor(prevValue / 10)\n      ) {\n        if (Math.floor(value / 10) > Math.floor(prevValue / 10)) {\n          prevTens.classList.add(\"slide-out-up\");\n          nextTens.classList.add(\"slide-in-up\");\n        } else {\n          prevTens.classList.add(\"slide-out-down\");\n          nextTens.classList.add(\"slide-in-down\");\n        }\n\n        const handleTensAnimationEnd = () => {\n          prevTens.classList.remove(\"slide-out-up\", \"slide-out-down\");\n          nextTens.classList.remove(\"slide-in-up\", \"slide-in-down\");\n          prevTens.removeEventListener(\"animationend\", handleTensAnimationEnd);\n        };\n\n        prevTens.addEventListener(\"animationend\", handleTensAnimationEnd);\n      }\n\n      if (prevOnes && nextOnes && value % 10 !== prevValue % 10) {\n        setTimeout(() => {\n          if (value % 10 > prevValue % 10) {\n            prevOnes.classList.add(\"slide-out-up\");\n            nextOnes.classList.add(\"slide-in-up\");\n          } else {\n            prevOnes.classList.add(\"slide-out-down\");\n            nextOnes.classList.add(\"slide-in-down\");\n          }\n\n          const handleOnesAnimationEnd = () => {\n            prevOnes.classList.remove(\"slide-out-up\", \"slide-out-down\");\n            nextOnes.classList.remove(\"slide-in-up\", \"slide-in-down\");\n            prevOnes.removeEventListener(\n              \"animationend\",\n              handleOnesAnimationEnd\n            );\n          };\n\n          prevOnes.addEventListener(\"animationend\", handleOnesAnimationEnd);\n        }, 50);\n      }\n\n      setPrevValue(value);\n    }\n  }, [value, prevValue]);\n\n  const formatValue = (val: number) => val.toString().padStart(2, \"0\");\n  const prevFormatted = formatValue(prevValue);\n  const currentFormatted = formatValue(value);\n\n  return (\n    <span className={cn(\"relative inline-flex items-center\", className)}>\n      <span className=\"relative inline-block overflow-hidden\">\n        <span\n          className=\"absolute inset-0 flex items-center justify-center\"\n          ref={prevTensRef}\n          style={{ transform: \"translateY(-100%)\" }}\n        >\n          {prevFormatted[0]}\n        </span>\n        <span\n          className=\"flex items-center justify-center\"\n          ref={nextTensRef}\n          style={{ transform: \"translateY(0%)\" }}\n        >\n          {currentFormatted[0]}\n        </span>\n      </span>\n      <span className=\"relative inline-block overflow-hidden\">\n        <span\n          className=\"absolute inset-0 flex items-center justify-center\"\n          ref={prevOnesRef}\n          style={{ transform: \"translateY(-100%)\" }}\n        >\n          {prevFormatted[1]}\n        </span>\n        <span\n          className=\"flex items-center justify-center\"\n          ref={nextOnesRef}\n          style={{ transform: \"translateY(0%)\" }}\n        >\n          {currentFormatted[1]}\n        </span>\n      </span>\n    </span>\n  );\n}\n\nexport function PricingSimple() {\n  const shouldReduceMotion = useReducedMotion();\n  const [isAnnual, setIsAnnual] = useState(true);\n\n  return (\n    <section>\n      <div className=\"relative bg-muted/50 py-16 md:py-32\">\n        <div className=\"mx-auto max-w-5xl px-6\">\n          <div className=\"mx-auto max-w-2xl text-center\">\n            <h2 className=\"text-balance font-bold text-3xl md:text-4xl lg:text-5xl lg:tracking-tight\">\n              Simple pricing for everyone\n            </h2>\n            <p className=\"mx-auto mt-4 max-w-xl text-balance text-foreground/70 text-lg\">\n              One plan, all features. No hidden fees, no complicated tiers.\n            </p>\n            <div className=\"my-12\">\n              <div\n                className=\"relative mx-auto grid w-fit grid-cols-2 rounded-full border bg-background p-1 *:block *:h-8 *:w-24 *:rounded-full *:text-foreground *:text-sm *:hover:opacity-75\"\n                data-period={isAnnual ? \"annually\" : \"monthly\"}\n              >\n                <div\n                  aria-hidden=\"true\"\n                  className={`pointer-events-none absolute inset-1 w-1/2 rounded-full border border-transparent bg-brand shadow ring-1 ring-foreground/5 transition-transform duration-500 ease-in-out ${\n                    isAnnual ? \"translate-x-full\" : \"translate-x-0\"\n                  }`}\n                />\n                <button\n                  className=\"relative duration-500 data-[active=true]:font-medium data-[active=true]:text-white\"\n                  data-active={!isAnnual}\n                  onClick={() => setIsAnnual(false)}\n                  type=\"button\"\n                >\n                  Monthly\n                </button>\n                <button\n                  className=\"relative duration-500 data-[active=true]:font-medium data-[active=true]:text-white\"\n                  data-active={isAnnual}\n                  onClick={() => setIsAnnual(true)}\n                  type=\"button\"\n                >\n                  Annually\n                </button>\n              </div>\n              <div className=\"mt-3 text-center text-xs\">\n                <span className=\"font-medium text-brand\">Save 20%</span> On\n                Annual Billing\n              </div>\n            </div>\n          </div>\n          <div className=\"container\">\n            <div className=\"mx-auto max-w-md\">\n              <motion.div\n                animate={\n                  shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }\n                }\n                className=\"group relative flex h-[650px] cursor-pointer flex-col overflow-hidden rounded-2xl border bg-background p-8\"\n                data-animate-card\n                initial={\n                  shouldReduceMotion ? { opacity: 1 } : { opacity: 0, y: 40 }\n                }\n                transition={\n                  shouldReduceMotion\n                    ? { duration: 0 }\n                    : { duration: 0.3, ease: [0.22, 1, 0.36, 1] }\n                }\n              >\n                {/* Gradient Accent */}\n                <div className=\"gradient-accent absolute top-0 right-0 h-4 w-32 rounded-bl-2xl bg-gradient-to-r from-green-400 via-blue-400 to-purple-400\" />\n\n                <div className=\"card-content relative z-10 flex h-full flex-col\">\n                  {/* Title */}\n                  <h3 className=\"mb-4 font-bold text-2xl text-foreground\">\n                    Pro\n                  </h3>\n                  {/* Price & Duration */}\n                  <div className=\"mb-6\">\n                    <span className=\"font-semibold text-3xl text-foreground\">\n                      <PriceFlow value={isAnnual ? 15 : 19} />€\n                    </span>\n                    <span className=\"mx-2 text-foreground/70\">•</span>\n                    <span className=\"text-foreground/70\">\n                      Perfect for individuals\n                    </span>\n                  </div>\n                  {/* CTA Button */}\n                  <SmoothButton className=\"mb-6 w-full\" variant=\"candy\">\n                    Get Started\n                  </SmoothButton>\n                  {/* Description */}\n                  <p className=\"mb-6 flex-grow text-foreground/70 text-sm leading-relaxed\">\n                    Everything you need to build and deploy amazing\n                    applications. Simple, powerful, and affordable.\n                  </p>\n                  {/* What's Included */}\n                  <div className=\"space-y-4\">\n                    <h4 className=\"font-medium text-foreground/70 text-xs uppercase tracking-wider\">\n                      What&apos;s included:\n                    </h4>\n                    <ul className=\"space-y-3\">\n                      {[\n                        \"Unlimited Projects\",\n                        \"Email Support\",\n                        \"All Features\",\n                        \"Advanced Analytics\",\n                        \"Team Collaboration\",\n                        \"Custom Domains\",\n                        \"Priority Updates\",\n                        \"API Access\",\n                      ].map((item) => (\n                        <li\n                          className=\"flex items-center gap-3 text-foreground text-sm\"\n                          key={item}\n                        >\n                          <div className=\"flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-full bg-foreground\">\n                            <svg\n                              aria-hidden=\"true\"\n                              className=\"h-2 w-2 text-background\"\n                              fill=\"currentColor\"\n                              viewBox=\"0 0 20 20\"\n                            >\n                              <path\n                                clipRule=\"evenodd\"\n                                d=\"M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z\"\n                                fillRule=\"evenodd\"\n                              />\n                            </svg>\n                          </div>\n                          {item}\n                        </li>\n                      ))}\n                    </ul>\n                  </div>\n                </div>\n              </motion.div>\n            </div>\n          </div>\n        </div>\n      </div>\n    </section>\n  );\n}\n\nexport default PricingSimple;\n","path":"index.tsx","target":"components/smoothui/pricing-1/index.tsx","type":"registry:block"}],"name":"pricing-1","registryDependencies":["https://smoothui.dev/r/smooth-button.json","https://smoothui.dev/r/tokens.json"],"title":"Pricing 1","type":"registry:block"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"Modern pricing block with three tiers","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport SmoothButton from \"@/components/smoothui/smooth-button\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { useEffect, useRef, useState } from \"react\";\n\nconst CARD_ANIMATION_DELAY = 0.1;\n\ninterface PriceFlowProps {\n  className?: string;\n  value: number;\n}\n\nfunction PriceFlow({ value, className = \"\" }: PriceFlowProps) {\n  const [prevValue, setPrevValue] = useState(value);\n  const prevTensRef = useRef<HTMLSpanElement>(null);\n  const nextTensRef = useRef<HTMLSpanElement>(null);\n  const prevOnesRef = useRef<HTMLSpanElement>(null);\n  const nextOnesRef = useRef<HTMLSpanElement>(null);\n\n  useEffect(() => {\n    if (value !== prevValue) {\n      const prevTens = prevTensRef.current;\n      const nextTens = nextTensRef.current;\n      const prevOnes = prevOnesRef.current;\n      const nextOnes = nextOnesRef.current;\n\n      if (\n        prevTens &&\n        nextTens &&\n        Math.floor(value / 10) !== Math.floor(prevValue / 10)\n      ) {\n        if (Math.floor(value / 10) > Math.floor(prevValue / 10)) {\n          prevTens.classList.add(\"slide-out-up\");\n          nextTens.classList.add(\"slide-in-up\");\n        } else {\n          prevTens.classList.add(\"slide-out-down\");\n          nextTens.classList.add(\"slide-in-down\");\n        }\n\n        const handleTensAnimationEnd = () => {\n          prevTens.classList.remove(\"slide-out-up\", \"slide-out-down\");\n          nextTens.classList.remove(\"slide-in-up\", \"slide-in-down\");\n          prevTens.removeEventListener(\"animationend\", handleTensAnimationEnd);\n        };\n\n        prevTens.addEventListener(\"animationend\", handleTensAnimationEnd);\n      }\n\n      if (prevOnes && nextOnes && value % 10 !== prevValue % 10) {\n        setTimeout(() => {\n          if (value % 10 > prevValue % 10) {\n            prevOnes.classList.add(\"slide-out-up\");\n            nextOnes.classList.add(\"slide-in-up\");\n          } else {\n            prevOnes.classList.add(\"slide-out-down\");\n            nextOnes.classList.add(\"slide-in-down\");\n          }\n\n          const handleOnesAnimationEnd = () => {\n            prevOnes.classList.remove(\"slide-out-up\", \"slide-out-down\");\n            nextOnes.classList.remove(\"slide-in-up\", \"slide-in-down\");\n            prevOnes.removeEventListener(\n              \"animationend\",\n              handleOnesAnimationEnd\n            );\n          };\n\n          prevOnes.addEventListener(\"animationend\", handleOnesAnimationEnd);\n        }, 50);\n      }\n\n      setPrevValue(value);\n    }\n  }, [value, prevValue]);\n\n  const formatValue = (val: number) => val.toString().padStart(2, \"0\");\n  const prevFormatted = formatValue(prevValue);\n  const currentFormatted = formatValue(value);\n\n  return (\n    <span className={cn(\"relative inline-flex items-center\", className)}>\n      <span className=\"relative inline-block overflow-hidden\">\n        <span\n          className=\"absolute inset-0 flex items-center justify-center\"\n          ref={prevTensRef}\n          style={{ transform: \"translateY(-100%)\" }}\n        >\n          {prevFormatted[0]}\n        </span>\n        <span\n          className=\"flex items-center justify-center\"\n          ref={nextTensRef}\n          style={{ transform: \"translateY(0%)\" }}\n        >\n          {currentFormatted[0]}\n        </span>\n      </span>\n      <span className=\"relative inline-block overflow-hidden\">\n        <span\n          className=\"absolute inset-0 flex items-center justify-center\"\n          ref={prevOnesRef}\n          style={{ transform: \"translateY(-100%)\" }}\n        >\n          {prevFormatted[1]}\n        </span>\n        <span\n          className=\"flex items-center justify-center\"\n          ref={nextOnesRef}\n          style={{ transform: \"translateY(0%)\" }}\n        >\n          {currentFormatted[1]}\n        </span>\n      </span>\n    </span>\n  );\n}\n\nexport function PricingModern() {\n  const shouldReduceMotion = useReducedMotion();\n  const [isAnnual, setIsAnnual] = useState(true);\n\n  return (\n    <section>\n      <div className=\"relative bg-muted/50 py-16 md:py-32\">\n        <div className=\"mx-auto max-w-5xl px-6\">\n          <div className=\"mx-auto max-w-2xl text-center\">\n            <h2 className=\"text-balance font-bold text-3xl md:text-4xl lg:text-5xl lg:tracking-tight\">\n              Choose your perfect plan\n            </h2>\n            <p className=\"mx-auto mt-4 max-w-xl text-balance text-foreground/70 text-lg\">\n              Modern pricing plans designed for teams of all sizes. Scale your\n              business with confidence.\n            </p>\n            <div className=\"my-12\">\n              <div\n                className=\"relative mx-auto grid w-fit grid-cols-2 rounded-full border bg-background p-1 *:block *:h-8 *:w-24 *:rounded-full *:text-foreground *:text-sm *:hover:opacity-75\"\n                data-period={isAnnual ? \"annually\" : \"monthly\"}\n              >\n                <div\n                  aria-hidden=\"true\"\n                  className={`pointer-events-none absolute inset-1 w-1/2 rounded-full border border-transparent bg-brand shadow ring-1 ring-foreground/5 transition-transform duration-500 ease-in-out ${\n                    isAnnual ? \"translate-x-full\" : \"translate-x-0\"\n                  }`}\n                />\n                <button\n                  className=\"relative duration-500 data-[active=true]:font-medium data-[active=true]:text-white\"\n                  data-active={!isAnnual}\n                  onClick={() => setIsAnnual(false)}\n                  type=\"button\"\n                >\n                  Monthly\n                </button>\n                <button\n                  className=\"relative duration-500 data-[active=true]:font-medium data-[active=true]:text-white\"\n                  data-active={isAnnual}\n                  onClick={() => setIsAnnual(true)}\n                  type=\"button\"\n                >\n                  Annually\n                </button>\n              </div>\n            </div>\n          </div>\n          <div className=\"container\">\n            <div className=\"mx-auto max-w-6xl\">\n              <div className=\"grid grid-cols-1 gap-6 *:p-8 md:grid-cols-3\">\n                {/* Basic Plan */}\n                <motion.div\n                  animate={\n                    shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }\n                  }\n                  className=\"group relative flex h-[650px] cursor-pointer flex-col overflow-hidden rounded-2xl border bg-background p-8\"\n                  data-animate-card\n                  initial={\n                    shouldReduceMotion ? { opacity: 1 } : { opacity: 0, y: 40 }\n                  }\n                  transition={\n                    shouldReduceMotion\n                      ? { duration: 0 }\n                      : { duration: 0.3, ease: [0.22, 1, 0.36, 1] }\n                  }\n                >\n                  <div className=\"card-content relative z-10 flex h-full flex-col\">\n                    {/* Title */}\n                    <h3 className=\"mb-4 font-bold text-2xl text-foreground\">\n                      Basic\n                    </h3>\n                    {/* Price & Duration */}\n                    <div className=\"mb-6\">\n                      <span className=\"font-semibold text-3xl text-foreground\">\n                        <PriceFlow value={isAnnual ? 10 : 15} />€\n                      </span>\n                      <span className=\"mx-2 text-foreground/70\">•</span>\n                      <span className=\"text-foreground/70\">\n                        Perfect for startups\n                      </span>\n                    </div>\n                    {/* CTA Button */}\n                    <SmoothButton className=\"mb-6 w-full\" variant=\"outline\">\n                      Get Started\n                    </SmoothButton>\n                    {/* Description */}\n                    <p className=\"mb-6 flex-grow text-foreground/70 text-sm leading-relaxed\">\n                      Essential features for growing businesses. Get started\n                      with everything you need to launch your project.\n                    </p>\n                    {/* What's Included */}\n                    <div className=\"space-y-4\">\n                      <h4 className=\"font-medium text-foreground/70 text-xs uppercase tracking-wider\">\n                        What&apos;s included:\n                      </h4>\n                      <ul className=\"space-y-3\">\n                        {[\n                          \"1 Project\",\n                          \"Email Support\",\n                          \"Core Features\",\n                          \"Basic Analytics\",\n                          \"Standard Security\",\n                          \"Community Access\",\n                        ].map((item) => (\n                          <li\n                            className=\"flex items-center gap-3 text-foreground text-sm\"\n                            key={item}\n                          >\n                            <div className=\"flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-full bg-foreground\">\n                              <svg\n                                aria-hidden=\"true\"\n                                className=\"h-2 w-2 text-background\"\n                                fill=\"currentColor\"\n                                viewBox=\"0 0 20 20\"\n                              >\n                                <path\n                                  clipRule=\"evenodd\"\n                                  d=\"M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z\"\n                                  fillRule=\"evenodd\"\n                                />\n                              </svg>\n                            </div>\n                            {item}\n                          </li>\n                        ))}\n                      </ul>\n                    </div>\n                  </div>\n                </motion.div>\n                {/* Pro Plan (Featured) */}\n                <motion.div\n                  animate={\n                    shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }\n                  }\n                  className=\"group relative flex h-[650px] cursor-pointer flex-col overflow-hidden rounded-2xl border bg-primary p-8\"\n                  data-animate-card\n                  initial={\n                    shouldReduceMotion ? { opacity: 1 } : { opacity: 0, y: 40 }\n                  }\n                  transition={\n                    shouldReduceMotion\n                      ? { duration: 0 }\n                      : {\n                          delay: CARD_ANIMATION_DELAY,\n                          duration: 0.3,\n                          ease: [0.22, 1, 0.36, 1],\n                        }\n                  }\n                >\n                  {/* Gradient Accent */}\n                  <div className=\"gradient-accent absolute top-0 right-0 h-4 w-32 rounded-bl-2xl bg-gradient-to-r from-blue-400 via-purple-400 to-pink-400\" />\n\n                  <div className=\"card-content relative z-10 flex h-full flex-col\">\n                    {/* Title */}\n                    <h3 className=\"mb-4 flex items-center gap-2 font-bold text-2xl text-foreground\">\n                      Pro{\" \"}\n                      <div className=\"rounded-full bg-brand px-2 py-1 font-bold text-white text-xs\">\n                        Most Popular\n                      </div>\n                    </h3>\n                    {/* Price & Duration */}\n                    <div className=\"mb-6\">\n                      <span className=\"font-semibold text-3xl text-foreground\">\n                        <PriceFlow value={isAnnual ? 25 : 35} />€\n                      </span>\n                      <span className=\"mx-2 text-foreground/70\">•</span>\n                      <span className=\"text-foreground/70\">Best for teams</span>\n                    </div>\n                    {/* CTA Button */}\n                    <SmoothButton className=\"mb-6 w-full\" variant=\"candy\">\n                      Get Started\n                    </SmoothButton>\n                    {/* Description */}\n                    <p className=\"mb-6 flex-grow text-foreground/70 text-sm leading-relaxed\">\n                      Advanced features for professional teams. Everything you\n                      need to scale your business with confidence.\n                    </p>\n                    {/* What's Included */}\n                    <div className=\"space-y-4\">\n                      <h4 className=\"font-medium text-foreground/70 text-xs uppercase tracking-wider\">\n                        What&apos;s included:\n                      </h4>\n                      <ul className=\"space-y-3\">\n                        {[\n                          \"Unlimited Projects\",\n                          \"Priority Support\",\n                          \"Team Collaboration\",\n                          \"Advanced Analytics\",\n                          \"Custom Integrations\",\n                          \"API Access\",\n                          \"White-label Options\",\n                          \"Priority Updates\",\n                        ].map((item) => (\n                          <li\n                            className=\"flex items-center gap-3 text-foreground text-sm\"\n                            key={item}\n                          >\n                            <div className=\"flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-full bg-foreground\">\n                              <svg\n                                aria-hidden=\"true\"\n                                className=\"h-2 w-2 text-background\"\n                                fill=\"currentColor\"\n                                viewBox=\"0 0 20 20\"\n                              >\n                                <path\n                                  clipRule=\"evenodd\"\n                                  d=\"M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z\"\n                                  fillRule=\"evenodd\"\n                                />\n                              </svg>\n                            </div>\n                            {item}\n                          </li>\n                        ))}\n                      </ul>\n                    </div>\n                  </div>\n                </motion.div>\n                {/* Enterprise Plan */}\n                <motion.div\n                  animate={\n                    shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }\n                  }\n                  className=\"group relative flex h-[650px] cursor-pointer flex-col overflow-hidden rounded-2xl border bg-background p-8\"\n                  data-animate-card\n                  initial={\n                    shouldReduceMotion ? { opacity: 1 } : { opacity: 0, y: 40 }\n                  }\n                  transition={\n                    shouldReduceMotion\n                      ? { duration: 0 }\n                      : {\n                          delay: CARD_ANIMATION_DELAY * 2,\n                          duration: 0.3,\n                          ease: [0.22, 1, 0.36, 1],\n                        }\n                  }\n                >\n                  <div className=\"card-content relative z-10 flex h-full flex-col\">\n                    {/* Title */}\n                    <h3 className=\"mb-4 font-bold text-2xl text-foreground\">\n                      Enterprise\n                    </h3>\n                    {/* Price & Duration */}\n                    <div className=\"mb-6\">\n                      <span className=\"font-semibold text-3xl text-foreground\">\n                        Custom\n                      </span>\n                      <span className=\"mx-2 text-foreground/70\">•</span>\n                      <span className=\"text-foreground/70\">\n                        Tailored solutions\n                      </span>\n                    </div>\n                    {/* CTA Button */}\n                    <SmoothButton className=\"mb-6 w-full\" variant=\"outline\">\n                      Contact Sales\n                    </SmoothButton>\n                    {/* Description */}\n                    <p className=\"mb-6 flex-grow text-foreground/70 text-sm leading-relaxed\">\n                      Custom solutions for large organizations. Get dedicated\n                      support and tailored features for your enterprise needs.\n                    </p>\n                    {/* What's Included */}\n                    <div className=\"space-y-4\">\n                      <h4 className=\"font-medium text-foreground/70 text-xs uppercase tracking-wider\">\n                        What&apos;s included:\n                      </h4>\n                      <ul className=\"space-y-3\">\n                        {[\n                          \"Everything in Pro\",\n                          \"Dedicated Manager\",\n                          \"Custom Integrations\",\n                          \"SLA & Support\",\n                          \"On-premise Options\",\n                          \"Advanced Security\",\n                          \"Training & Onboarding\",\n                          \"24/7 Phone Support\",\n                        ].map((item) => (\n                          <li\n                            className=\"flex items-center gap-3 text-foreground text-sm\"\n                            key={item}\n                          >\n                            <div className=\"flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-full bg-foreground\">\n                              <svg\n                                aria-hidden=\"true\"\n                                className=\"h-2 w-2 text-background\"\n                                fill=\"currentColor\"\n                                viewBox=\"0 0 20 20\"\n                              >\n                                <path\n                                  clipRule=\"evenodd\"\n                                  d=\"M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z\"\n                                  fillRule=\"evenodd\"\n                                />\n                              </svg>\n                            </div>\n                            {item}\n                          </li>\n                        ))}\n                      </ul>\n                    </div>\n                  </div>\n                </motion.div>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n    </section>\n  );\n}\n\nexport default PricingModern;\n","path":"index.tsx","target":"components/smoothui/pricing-2/index.tsx","type":"registry:block"}],"name":"pricing-2","registryDependencies":["https://smoothui.dev/r/smooth-button.json","https://smoothui.dev/r/tokens.json"],"title":"Pricing 2","type":"registry:block"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"Creative pricing block with two plans","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport SmoothButton from \"@/components/smoothui/smooth-button\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { useEffect, useRef, useState } from \"react\";\n\nconst CARD_ANIMATION_DELAY = 0.1;\n\ninterface PriceFlowProps {\n  className?: string;\n  value: number;\n}\n\nfunction PriceFlow({ value, className = \"\" }: PriceFlowProps) {\n  const [prevValue, setPrevValue] = useState(value);\n  const prevTensRef = useRef<HTMLSpanElement>(null);\n  const nextTensRef = useRef<HTMLSpanElement>(null);\n  const prevOnesRef = useRef<HTMLSpanElement>(null);\n  const nextOnesRef = useRef<HTMLSpanElement>(null);\n\n  useEffect(() => {\n    if (value !== prevValue) {\n      const prevTens = prevTensRef.current;\n      const nextTens = nextTensRef.current;\n      const prevOnes = prevOnesRef.current;\n      const nextOnes = nextOnesRef.current;\n\n      if (\n        prevTens &&\n        nextTens &&\n        Math.floor(value / 10) !== Math.floor(prevValue / 10)\n      ) {\n        if (Math.floor(value / 10) > Math.floor(prevValue / 10)) {\n          prevTens.classList.add(\"slide-out-up\");\n          nextTens.classList.add(\"slide-in-up\");\n        } else {\n          prevTens.classList.add(\"slide-out-down\");\n          nextTens.classList.add(\"slide-in-down\");\n        }\n\n        const handleTensAnimationEnd = () => {\n          prevTens.classList.remove(\"slide-out-up\", \"slide-out-down\");\n          nextTens.classList.remove(\"slide-in-up\", \"slide-in-down\");\n          prevTens.removeEventListener(\"animationend\", handleTensAnimationEnd);\n        };\n\n        prevTens.addEventListener(\"animationend\", handleTensAnimationEnd);\n      }\n\n      if (prevOnes && nextOnes && value % 10 !== prevValue % 10) {\n        setTimeout(() => {\n          if (value % 10 > prevValue % 10) {\n            prevOnes.classList.add(\"slide-out-up\");\n            nextOnes.classList.add(\"slide-in-up\");\n          } else {\n            prevOnes.classList.add(\"slide-out-down\");\n            nextOnes.classList.add(\"slide-in-down\");\n          }\n\n          const handleOnesAnimationEnd = () => {\n            prevOnes.classList.remove(\"slide-out-up\", \"slide-out-down\");\n            nextOnes.classList.remove(\"slide-in-up\", \"slide-in-down\");\n            prevOnes.removeEventListener(\n              \"animationend\",\n              handleOnesAnimationEnd\n            );\n          };\n\n          prevOnes.addEventListener(\"animationend\", handleOnesAnimationEnd);\n        }, 50);\n      }\n\n      setPrevValue(value);\n    }\n  }, [value, prevValue]);\n\n  const formatValue = (val: number) => val.toString().padStart(2, \"0\");\n  const prevFormatted = formatValue(prevValue);\n  const currentFormatted = formatValue(value);\n\n  return (\n    <span className={cn(\"relative inline-flex items-center\", className)}>\n      <span className=\"relative inline-block overflow-hidden\">\n        <span\n          className=\"absolute inset-0 flex items-center justify-center\"\n          ref={prevTensRef}\n          style={{ transform: \"translateY(-100%)\" }}\n        >\n          {prevFormatted[0]}\n        </span>\n        <span\n          className=\"flex items-center justify-center\"\n          ref={nextTensRef}\n          style={{ transform: \"translateY(0%)\" }}\n        >\n          {currentFormatted[0]}\n        </span>\n      </span>\n      <span className=\"relative inline-block overflow-hidden\">\n        <span\n          className=\"absolute inset-0 flex items-center justify-center\"\n          ref={prevOnesRef}\n          style={{ transform: \"translateY(-100%)\" }}\n        >\n          {prevFormatted[1]}\n        </span>\n        <span\n          className=\"flex items-center justify-center\"\n          ref={nextOnesRef}\n          style={{ transform: \"translateY(0%)\" }}\n        >\n          {currentFormatted[1]}\n        </span>\n      </span>\n    </span>\n  );\n}\n\nexport function PricingCreative() {\n  const shouldReduceMotion = useReducedMotion();\n  const [isAnnual, setIsAnnual] = useState(true);\n\n  return (\n    <section>\n      <div className=\"relative bg-muted/50 py-16 md:py-32\">\n        <div className=\"mx-auto max-w-5xl px-6\">\n          <div className=\"mx-auto max-w-2xl text-center\">\n            <h2 className=\"text-balance font-bold text-3xl md:text-4xl lg:text-5xl lg:tracking-tight\">\n              Deploy faster with Vercel Pro\n            </h2>\n            <p className=\"mx-auto mt-4 max-w-xl text-balance text-foreground/70 text-lg\">\n              Scale your applications with advanced deployment features and\n              priority support\n            </p>\n            <div className=\"my-12\">\n              <div\n                className=\"relative mx-auto grid w-fit grid-cols-2 rounded-full border bg-background p-1 *:block *:h-8 *:w-24 *:rounded-full *:text-foreground *:text-sm *:hover:opacity-75\"\n                data-period={isAnnual ? \"annually\" : \"monthly\"}\n              >\n                <div\n                  aria-hidden=\"true\"\n                  className={`pointer-events-none absolute inset-1 w-1/2 rounded-full border border-transparent bg-brand shadow ring-1 ring-foreground/5 transition-transform duration-500 ease-in-out ${\n                    isAnnual ? \"translate-x-full\" : \"translate-x-0\"\n                  }`}\n                />\n                <button\n                  className=\"relative duration-500 data-[active=true]:font-medium data-[active=true]:text-white\"\n                  data-active={!isAnnual}\n                  onClick={() => setIsAnnual(false)}\n                  type=\"button\"\n                >\n                  Monthly\n                </button>\n                <button\n                  className=\"relative duration-500 data-[active=true]:font-medium data-[active=true]:text-white\"\n                  data-active={isAnnual}\n                  onClick={() => setIsAnnual(true)}\n                  type=\"button\"\n                >\n                  Annually\n                </button>\n              </div>\n            </div>\n          </div>\n          <div className=\"container\">\n            <div className=\"mx-auto max-w-5xl\">\n              <div className=\"grid grid-cols-2 gap-6 *:p-8\">\n                {/* Free Plan */}\n                <motion.div\n                  animate={\n                    shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }\n                  }\n                  className=\"group relative flex h-[650px] cursor-pointer flex-col overflow-hidden rounded-2xl border bg-background p-8\"\n                  data-animate-card\n                  initial={\n                    shouldReduceMotion ? { opacity: 1 } : { opacity: 0, y: 40 }\n                  }\n                  transition={\n                    shouldReduceMotion\n                      ? { duration: 0 }\n                      : { duration: 0.3, ease: [0.22, 1, 0.36, 1] }\n                  }\n                >\n                  <div className=\"card-content relative z-10 flex h-full flex-col\">\n                    {/* Title */}\n                    <h3 className=\"mb-4 font-bold text-2xl text-foreground\">\n                      Hobby\n                    </h3>\n\n                    {/* Price & Duration */}\n                    <div className=\"mb-6\">\n                      <span className=\"font-semibold text-3xl text-foreground\">\n                        <PriceFlow value={0} />€\n                      </span>\n                      <span className=\"mx-2 text-foreground/70\">•</span>\n                      <span className=\"text-foreground/70\">\n                        Perfect for personal projects\n                      </span>\n                    </div>\n\n                    {/* CTA Button */}\n                    <SmoothButton className=\"mb-6 w-full\" variant=\"outline\">\n                      Get Started\n                    </SmoothButton>\n\n                    {/* Description */}\n                    <p className=\"mb-6 flex-grow text-foreground/70 text-sm leading-relaxed\">\n                      Deploy your personal projects and experiment with the\n                      latest web technologies. Perfect for developers learning\n                      and building side projects.\n                    </p>\n\n                    {/* What's Included */}\n                    <div className=\"space-y-4\">\n                      <h4 className=\"font-medium text-foreground/70 text-xs uppercase tracking-wider\">\n                        What&apos;s included:\n                      </h4>\n                      <ul className=\"space-y-3\">\n                        {[\n                          \"100GB Bandwidth\",\n                          \"Unlimited Personal Projects\",\n                          \"Community Support\",\n                          \"Automatic HTTPS\",\n                          \"Global CDN\",\n                          \"Preview Deployments\",\n                        ].map((item) => (\n                          <li\n                            className=\"flex items-center gap-3 text-foreground text-sm\"\n                            key={item}\n                          >\n                            <div className=\"flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-full bg-foreground\">\n                              <svg\n                                aria-hidden=\"true\"\n                                className=\"h-2 w-2 text-background\"\n                                fill=\"currentColor\"\n                                viewBox=\"0 0 20 20\"\n                              >\n                                <path\n                                  clipRule=\"evenodd\"\n                                  d=\"M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z\"\n                                  fillRule=\"evenodd\"\n                                />\n                              </svg>\n                            </div>\n                            {item}\n                          </li>\n                        ))}\n                      </ul>\n                    </div>\n                  </div>\n                </motion.div>\n                {/* Pro Plan */}\n                <motion.div\n                  animate={\n                    shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }\n                  }\n                  className=\"group relative flex h-[650px] cursor-pointer flex-col overflow-hidden rounded-2xl border bg-primary p-8\"\n                  data-animate-card\n                  initial={\n                    shouldReduceMotion ? { opacity: 1 } : { opacity: 0, y: 40 }\n                  }\n                  transition={\n                    shouldReduceMotion\n                      ? { duration: 0 }\n                      : {\n                          delay: CARD_ANIMATION_DELAY,\n                          duration: 0.3,\n                          ease: [0.22, 1, 0.36, 1],\n                        }\n                  }\n                >\n                  {/* Gradient Accent */}\n                  <div className=\"gradient-accent absolute top-0 right-0 h-4 w-32 rounded-bl-2xl bg-gradient-to-r from-purple-400 via-pink-400 to-orange-400\" />\n\n                  <div className=\"card-content relative z-10 flex h-full flex-col\">\n                    {/* Title */}\n                    <h3 className=\"mb-4 font-bold text-2xl text-foreground\">\n                      Pro\n                    </h3>\n\n                    {/* Price & Duration */}\n                    <div className=\"mb-6\">\n                      <span className=\"font-semibold text-3xl text-foreground\">\n                        <PriceFlow value={isAnnual ? 12 : 25} />€\n                      </span>\n                      <span className=\"mx-2 text-foreground/70\">•</span>\n                      <span className=\"text-foreground/70\">Per month</span>\n                    </div>\n\n                    {/* CTA Button */}\n                    <SmoothButton className=\"mb-6 w-full\" variant=\"candy\">\n                      Get Started\n                    </SmoothButton>\n\n                    {/* Description */}\n                    <p className=\"mb-6 flex-grow text-foreground/70 text-sm leading-relaxed\">\n                      Advanced deployment features for production applications.\n                      Perfect for teams building and scaling professional web\n                      applications.\n                    </p>\n\n                    {/* What's Included */}\n                    <div className=\"space-y-4\">\n                      <h4 className=\"font-medium text-foreground/70 text-xs uppercase tracking-wider\">\n                        What&apos;s included:\n                      </h4>\n                      <ul className=\"space-y-3\">\n                        {[\n                          \"Everything in Hobby\",\n                          \"Unlimited Bandwidth\",\n                          \"Team Collaboration\",\n                          \"Priority Support\",\n                          \"Advanced Analytics\",\n                          \"Password Protection\",\n                          \"Custom Domains\",\n                          \"Edge Functions\",\n                          \"Cron Jobs\",\n                          \"Webhooks\",\n                        ].map((item) => (\n                          <li\n                            className=\"flex items-center gap-3 text-foreground text-sm\"\n                            key={item}\n                          >\n                            <div className=\"flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-full bg-foreground\">\n                              <svg\n                                aria-hidden=\"true\"\n                                className=\"h-2 w-2 text-background\"\n                                fill=\"currentColor\"\n                                viewBox=\"0 0 20 20\"\n                              >\n                                <path\n                                  clipRule=\"evenodd\"\n                                  d=\"M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z\"\n                                  fillRule=\"evenodd\"\n                                />\n                              </svg>\n                            </div>\n                            {item}\n                          </li>\n                        ))}\n                      </ul>\n                    </div>\n                  </div>\n                </motion.div>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n    </section>\n  );\n}\n\nexport default PricingCreative;\n","path":"index.tsx","target":"components/smoothui/pricing-3/index.tsx","type":"registry:block"}],"name":"pricing-3","registryDependencies":["https://smoothui.dev/r/smooth-button.json","https://smoothui.dev/r/tokens.json"],"title":"Pricing 3","type":"registry:block"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["lucide-react","motion","class-variance-authority","@radix-ui/react-slot"],"description":"Shared components for SmoothUI blocks","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { motion, useReducedMotion, type Variants } from \"motion/react\";\nimport React, { type ReactNode } from \"react\";\n\nexport type PresetType =\n  | \"fade\"\n  | \"slide\"\n  | \"scale\"\n  | \"blur\"\n  | \"blur-slide\"\n  | \"zoom\"\n  | \"flip\"\n  | \"bounce\"\n  | \"rotate\"\n  | \"swing\";\n\nexport interface AnimatedGroupProps {\n  as?: React.ElementType;\n  asChild?: React.ElementType;\n  children: ReactNode;\n  className?: string;\n  preset?: PresetType;\n  variants?: {\n    container?: Variants;\n    item?: Variants;\n  };\n}\n\nconst defaultContainerVariants: Variants = {\n  visible: {\n    transition: {\n      staggerChildren: 0.1,\n    },\n  },\n};\n\nconst defaultItemVariants: Variants = {\n  hidden: { opacity: 0 },\n  visible: { opacity: 1 },\n};\n\nconst presetVariants: Record<PresetType, Variants> = {\n  blur: {\n    hidden: { filter: \"blur(4px)\" },\n    visible: { filter: \"blur(0px)\" },\n  },\n  \"blur-slide\": {\n    hidden: { filter: \"blur(4px)\", y: 20 },\n    visible: { filter: \"blur(0px)\", y: 0 },\n  },\n  bounce: {\n    hidden: { y: -50 },\n    visible: {\n      transition: { damping: 10, stiffness: 400, type: \"spring\" as const },\n      y: 0,\n    },\n  },\n  fade: {},\n  flip: {\n    hidden: { rotateX: -90 },\n    visible: {\n      rotateX: 0,\n      transition: { damping: 20, stiffness: 300, type: \"spring\" as const },\n    },\n  },\n  rotate: {\n    hidden: { rotate: -180 },\n    visible: {\n      rotate: 0,\n      transition: { damping: 15, stiffness: 200, type: \"spring\" as const },\n    },\n  },\n  scale: {\n    hidden: { scale: 0.8 },\n    visible: { scale: 1 },\n  },\n  slide: {\n    hidden: { y: 20 },\n    visible: { y: 0 },\n  },\n  swing: {\n    hidden: { rotate: -10 },\n    visible: {\n      rotate: 0,\n      transition: { damping: 8, stiffness: 300, type: \"spring\" as const },\n    },\n  },\n  zoom: {\n    hidden: { scale: 0.5 },\n    visible: {\n      scale: 1,\n      transition: { damping: 20, stiffness: 300, type: \"spring\" as const },\n    },\n  },\n};\n\nconst reducedContainerVariants: Variants = {\n  hidden: { opacity: 1 },\n  visible: { opacity: 1, transition: { staggerChildren: 0 } },\n};\n\nconst reducedItemVariants: Variants = {\n  hidden: { opacity: 1 },\n  visible: { opacity: 1 },\n};\n\nconst addDefaultVariants = (variants: Variants) => ({\n  hidden: { ...defaultItemVariants.hidden, ...variants.hidden },\n  visible: { ...defaultItemVariants.visible, ...variants.visible },\n});\n\nfunction AnimatedGroup({\n  children,\n  className,\n  variants,\n  preset,\n  as = \"div\",\n  asChild = \"div\",\n}: AnimatedGroupProps) {\n  const shouldReduceMotion = useReducedMotion();\n\n  const selectedVariants = {\n    container: addDefaultVariants(defaultContainerVariants),\n    item: addDefaultVariants(preset ? presetVariants[preset] : {}),\n  };\n  const containerVariants = shouldReduceMotion\n    ? reducedContainerVariants\n    : (variants?.container ?? selectedVariants.container);\n  const itemVariants = shouldReduceMotion\n    ? reducedItemVariants\n    : (variants?.item ?? selectedVariants.item);\n\n  const MotionComponent = motion(as);\n\n  const MotionChild = motion(asChild);\n\n  return (\n    <MotionComponent\n      animate=\"visible\"\n      className={className}\n      initial=\"hidden\"\n      variants={containerVariants}\n    >\n      {React.Children.map(children, (child, index) => (\n        <MotionChild\n          // biome-ignore lint/suspicious/noArrayIndexKey: React.Children order is stable\n          key={index}\n          variants={itemVariants}\n        >\n          {child}\n        </MotionChild>\n      ))}\n    </MotionComponent>\n  );\n}\n\nexport { AnimatedGroup };\n","path":"animated-group.tsx","target":"components/smoothui/shared/animated-group.tsx","type":"registry:component"},{"content":"\"use client\";\n\nimport { motion, useReducedMotion } from \"motion/react\";\nimport type { ElementType, ReactNode } from \"react\";\n\ninterface AnimatedTextProps {\n  as?: ElementType;\n  children: ReactNode;\n  className?: string;\n  delay?: number;\n}\n\nexport function AnimatedText({\n  as: Tag = \"span\",\n  children,\n  className,\n  delay = 0,\n}: AnimatedTextProps) {\n  const shouldReduceMotion = useReducedMotion();\n\n  return (\n    <motion.div\n      animate={\n        shouldReduceMotion\n          ? { opacity: 1 }\n          : { filter: \"blur(0px)\", opacity: 1, y: 0 }\n      }\n      initial={\n        shouldReduceMotion\n          ? { opacity: 1 }\n          : { filter: \"blur(12px)\", opacity: 0, y: 12 }\n      }\n      transition={\n        shouldReduceMotion\n          ? { duration: 0 }\n          : {\n              bounce: 0.3,\n              delay,\n              duration: 1.5,\n              type: \"spring\" as const,\n            }\n      }\n    >\n      <Tag className={className}>{children}</Tag>\n    </motion.div>\n  );\n}\n","path":"animated-text.tsx","target":"components/smoothui/shared/animated-text.tsx","type":"registry:component"},{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport SmoothButton from \"@/components/smoothui/smooth-button\";\nimport { Menu, X } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useState } from \"react\";\n\nconst menuItems = [\n  { href: \"#link\", id: \"features\", name: \"Features\" },\n  { href: \"#link\", id: \"pricing\", name: \"Pricing\" },\n  { href: \"#link\", id: \"about\", name: \"About\" },\n];\n\n// Animation constants\nconst ANIMATION_DURATION = 0.2;\nconst STAGGER_DELAY = 0.05;\nconst EASE_OUT_QUART_X1 = 0.22;\nconst EASE_OUT_QUART_Y1 = 1;\nconst EASE_OUT_QUART_X2 = 0.36;\nconst EASE_OUT_QUART_Y2 = 1;\nconst EASE_OUT_QUART = [\n  EASE_OUT_QUART_X1,\n  EASE_OUT_QUART_Y1,\n  EASE_OUT_QUART_X2,\n  EASE_OUT_QUART_Y2,\n] as const;\nconst ROTATION_ANGLE = 180;\nconst SCALE_MIN = 0;\nconst SCALE_MAX = 1;\nconst TRANSLATE_Y_OFFSET = -10;\nconst TRANSLATE_X_OFFSET = -10;\n\nexport const HeroHeader = () => {\n  const shouldReduceMotion = useReducedMotion();\n  const [menuState, setMenuState] = useState(false);\n\n  return (\n    <div className=\"relative\">\n      <header>\n        <nav className=\"absolute top-0 left-0 z-20 w-full transition-all duration-300\">\n          <div className=\"mx-auto max-w-5xl px-6\">\n            <div className=\"relative flex flex-wrap items-center justify-between gap-6 py-6 transition-all duration-200 lg:gap-0\">\n              <div className=\"flex w-full justify-between gap-6 lg:w-auto\">\n                <a\n                  aria-label=\"SmoothUI home\"\n                  className=\"flex items-center gap-2\"\n                  href=\"/\"\n                >\n                  <span className=\"sr-only\">SmoothUI</span>\n                  <img\n                    alt=\"SmoothUI logo\"\n                    className={cn(\"h-6 w-auto\", \"dark:hidden\")}\n                    draggable={false}\n                    height={120}\n                    src=\"/brand/logo-smoothui-light-1000w.png\"\n                    width={609}\n                  />\n                  <img\n                    alt=\"SmoothUI logo\"\n                    className={cn(\"hidden h-6 w-auto\", \"dark:block\")}\n                    draggable={false}\n                    height={120}\n                    src=\"/brand/logo-smoothui-dark-1000w.png\"\n                    width={609}\n                  />\n                </a>\n\n                <button\n                  aria-label={menuState === true ? \"Close Menu\" : \"Open Menu\"}\n                  className=\"relative z-20 -m-2.5 -mr-4 block cursor-pointer p-2.5 lg:hidden\"\n                  onClick={() => setMenuState(!menuState)}\n                  type=\"button\"\n                >\n                  <AnimatePresence mode=\"wait\">\n                    {menuState ? (\n                      <motion.div\n                        animate={\n                          shouldReduceMotion\n                            ? { opacity: 1 }\n                            : { opacity: 1, rotate: 0, scale: SCALE_MAX }\n                        }\n                        exit={\n                          shouldReduceMotion\n                            ? { opacity: 0, transition: { duration: 0 } }\n                            : {\n                                opacity: 0,\n                                rotate: -ROTATION_ANGLE,\n                                scale: SCALE_MIN,\n                              }\n                        }\n                        initial={\n                          shouldReduceMotion\n                            ? { opacity: 1 }\n                            : {\n                                opacity: 0,\n                                rotate: ROTATION_ANGLE,\n                                scale: SCALE_MIN,\n                              }\n                        }\n                        key=\"close\"\n                        transition={\n                          shouldReduceMotion\n                            ? { duration: 0 }\n                            : {\n                                duration: ANIMATION_DURATION,\n                                ease: EASE_OUT_QUART,\n                              }\n                        }\n                      >\n                        <X className=\"m-auto size-6\" />\n                      </motion.div>\n                    ) : (\n                      <motion.div\n                        animate={\n                          shouldReduceMotion\n                            ? { opacity: 1 }\n                            : { opacity: 1, rotate: 0, scale: SCALE_MAX }\n                        }\n                        exit={\n                          shouldReduceMotion\n                            ? { opacity: 0, transition: { duration: 0 } }\n                            : {\n                                opacity: 0,\n                                rotate: ROTATION_ANGLE,\n                                scale: SCALE_MIN,\n                              }\n                        }\n                        initial={\n                          shouldReduceMotion\n                            ? { opacity: 1 }\n                            : {\n                                opacity: 0,\n                                rotate: -ROTATION_ANGLE,\n                                scale: SCALE_MIN,\n                              }\n                        }\n                        key=\"menu\"\n                        transition={\n                          shouldReduceMotion\n                            ? { duration: 0 }\n                            : {\n                                duration: ANIMATION_DURATION,\n                                ease: EASE_OUT_QUART,\n                              }\n                        }\n                      >\n                        <Menu className=\"m-auto size-6\" />\n                      </motion.div>\n                    )}\n                  </AnimatePresence>\n                </button>\n\n                <div className=\"m-auto hidden size-fit lg:block\">\n                  <ul className=\"flex gap-1\">\n                    {menuItems.map((item) => (\n                      <li key={item.id}>\n                        <SmoothButton asChild size=\"sm\" variant=\"ghost\">\n                          <a className=\"text-base\" href={item.href}>\n                            <span>{item.name}</span>\n                          </a>\n                        </SmoothButton>\n                      </li>\n                    ))}\n                  </ul>\n                </div>\n              </div>\n\n              <AnimatePresence>\n                {menuState ? (\n                  <motion.div\n                    animate={\n                      shouldReduceMotion\n                        ? { opacity: 1 }\n                        : { opacity: 1, scale: SCALE_MAX, y: 0 }\n                    }\n                    className=\"mb-6 w-full flex-wrap items-center justify-end space-y-8 rounded-3xl border bg-background p-6 shadow-2xl shadow-zinc-300/20 md:flex-nowrap lg:m-0 lg:flex lg:w-fit lg:gap-6 lg:space-y-0 lg:border-transparent lg:bg-transparent lg:p-0 lg:shadow-none dark:shadow-none dark:lg:bg-transparent\"\n                    exit={\n                      shouldReduceMotion\n                        ? { opacity: 0, transition: { duration: 0 } }\n                        : { opacity: 0, scale: 0.95, y: TRANSLATE_Y_OFFSET }\n                    }\n                    initial={\n                      shouldReduceMotion\n                        ? { opacity: 1 }\n                        : { opacity: 0, scale: 0.95, y: TRANSLATE_Y_OFFSET }\n                    }\n                    transition={\n                      shouldReduceMotion\n                        ? { duration: 0 }\n                        : {\n                            duration: ANIMATION_DURATION,\n                            ease: EASE_OUT_QUART,\n                          }\n                    }\n                  >\n                    <div className=\"lg:hidden\">\n                      <ul className=\"space-y-6 text-base\">\n                        {menuItems.map((item, index) => (\n                          <motion.li\n                            animate={\n                              shouldReduceMotion\n                                ? { opacity: 1 }\n                                : { opacity: 1, x: 0 }\n                            }\n                            initial={\n                              shouldReduceMotion\n                                ? { opacity: 1 }\n                                : { opacity: 0, x: TRANSLATE_X_OFFSET }\n                            }\n                            key={item.id}\n                            transition={\n                              shouldReduceMotion\n                                ? { duration: 0 }\n                                : {\n                                    delay: index * STAGGER_DELAY,\n                                    duration: ANIMATION_DURATION,\n                                    ease: EASE_OUT_QUART,\n                                  }\n                            }\n                          >\n                            <a\n                              className=\"block text-muted-foreground duration-150 hover:text-accent-foreground\"\n                              href={item.href}\n                              onClick={() => setMenuState(false)}\n                            >\n                              <span>{item.name}</span>\n                            </a>\n                          </motion.li>\n                        ))}\n                      </ul>\n                    </div>\n                    <motion.div\n                      animate={\n                        shouldReduceMotion\n                          ? { opacity: 1 }\n                          : { opacity: 1, y: 0 }\n                      }\n                      className=\"flex w-full flex-col space-y-3 sm:flex-row sm:gap-3 sm:space-y-0 md:w-fit\"\n                      initial={\n                        shouldReduceMotion\n                          ? { opacity: 1 }\n                          : { opacity: 0, y: 10 }\n                      }\n                      transition={\n                        shouldReduceMotion\n                          ? { duration: 0 }\n                          : {\n                              delay:\n                                menuItems.length * STAGGER_DELAY +\n                                STAGGER_DELAY,\n                              duration: ANIMATION_DURATION,\n                              ease: EASE_OUT_QUART,\n                            }\n                      }\n                    >\n                      <SmoothButton\n                        onClick={() => setMenuState(false)}\n                        size=\"sm\"\n                        type=\"button\"\n                        variant=\"ghost\"\n                      >\n                        <span>Login</span>\n                      </SmoothButton>\n                      <SmoothButton\n                        onClick={() => setMenuState(false)}\n                        size=\"sm\"\n                        type=\"button\"\n                      >\n                        <span>Sign Up</span>\n                      </SmoothButton>\n                    </motion.div>\n                  </motion.div>\n                ) : null}\n              </AnimatePresence>\n            </div>\n          </div>\n        </nav>\n      </header>\n    </div>\n  );\n};\n","path":"hero-header.tsx","target":"components/smoothui/shared/hero-header.tsx","type":"registry:component"},{"content":"export { default as Button } from \"@/components/smoothui/smooth-button\";\nexport type { AnimatedGroupProps, PresetType } from \"./animated-group\";\nexport { AnimatedGroup } from \"./animated-group\";\nexport { AnimatedText } from \"./animated-text\";\nexport { HeroHeader } from \"./hero-header\";\nexport {\n  Canpoy,\n  Canva,\n  Casetext,\n  Clearbit,\n  Descript,\n  Duolingo,\n  Faire,\n  IDEO,\n  KhanAcademy,\n  Quizlet,\n  Ramp,\n  Strava,\n} from \"./logos\";\n","path":"index.ts","target":"components/smoothui/shared/index.ts","type":"registry:component"},{"content":"import React from \"react\";\n\nfunction Canpoy() {\n  return (\n    <svg\n      aria-hidden=\"true\"\n      fill=\"none\"\n      height=\"136\"\n      preserveAspectRatio=\"xMidYMid meet\"\n      viewBox=\"0 0 136 136\"\n      width=\"136\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <g clipPath=\"url(#clip0_16329_10582)\">\n        <path\n          d=\"M94.9238 63.2042V78.329H97.4549V73.0816C98.4272 74.0385 99.7236 74.625 101.144 74.625C104.138 74.625 106.561 72.0476 106.561 68.8683C106.561 65.689 104.138 63.1116 101.144 63.1116C99.7236 63.1116 98.4272 63.698 97.4549 64.6549V63.2042H94.9238ZM97.4704 69.4239C97.4086 69.0535 97.4086 68.6831 97.4704 68.3127C97.5167 67.9886 97.6093 67.6799 97.7482 67.3712C98.489 65.7198 100.233 64.9173 101.884 65.4729C103.273 65.9359 104.199 67.2632 104.215 68.822C104.246 71.3685 101.838 73.0816 99.523 72.1556C98.3964 71.6926 97.6401 70.6123 97.4704 69.4239Z\"\n          fill=\"currentColor\"\n        />\n        <path\n          d=\"M64.5501 63.1424V64.6549C63.5778 63.698 62.2814 63.1116 60.8615 63.1116C57.8674 63.1116 55.4443 65.689 55.4443 68.8683C55.4443 72.0476 57.8674 74.625 60.8615 74.625C62.2814 74.625 63.5778 74.0385 64.5501 73.0816V74.5478H67.0812V63.127H64.5501V63.1424ZM62.4974 72.1402C60.1824 73.0662 57.7594 71.3531 57.8057 68.8065C57.8365 67.2477 58.7625 65.9205 60.1361 65.4575C61.7875 64.9019 63.5161 65.7044 64.2723 67.3558C64.4112 67.6645 64.5038 67.9731 64.5501 68.2972C64.6118 68.6676 64.6118 69.038 64.5501 69.4084C64.3649 70.6123 63.6241 71.6926 62.4974 72.1402Z\"\n          fill=\"currentColor\"\n        />\n        <path\n          d=\"M38.6221 74.5479L32.6802 58.9755C32.2017 57.7562 31.0751 57 29.8404 57H26.3524C25.1332 57 23.9911 57.7562 23.5281 58.9755L17.6325 74.1466C17.4936 74.4862 17.401 74.8411 17.3393 75.1961C17.3084 75.3504 17.293 75.5202 17.293 75.69C17.293 77.3877 18.6202 78.7149 20.3179 78.7149H35.7977C35.7977 78.7149 35.7977 78.7149 35.8132 78.7149C37.0015 78.7149 38.0819 77.9896 38.5603 76.9092C38.8536 76.1838 38.8999 75.3504 38.6221 74.5479ZM30.7818 75.5974H21.5372C21.4446 75.5974 21.1976 75.5665 21.0742 75.4893C20.6575 75.2115 20.3951 74.7485 20.5803 74.1775L25.2104 62.4789C25.303 62.2011 25.5808 61.8461 26.1981 61.8461C26.5994 61.8461 26.9543 62.0622 27.1087 62.4789L31.7696 74.1775C31.8313 74.3164 31.8313 74.4553 31.8313 74.5479C31.8313 75.1189 31.3529 75.5974 30.7818 75.5974Z\"\n          fill=\"currentColor\"\n        />\n        <path\n          d=\"M54.4877 72.2484C53.9321 73.0046 53.2376 73.5911 52.4042 74.0232C51.4936 74.4862 50.4904 74.7177 49.4101 74.7177C47.6507 74.7177 46.2154 74.1621 45.0578 73.0663C43.9158 71.9706 43.3447 70.5661 43.3447 68.853C43.3447 67.109 43.9312 65.6737 45.0887 64.547C46.2617 63.4204 47.7124 62.8494 49.441 62.8494C50.475 62.8494 51.4319 63.0654 52.3116 63.4821C53.037 63.8371 53.6697 64.3155 54.1945 64.9175L52.2344 66.7386C51.4627 65.7817 50.5367 65.3033 49.4255 65.3033C48.4686 65.3033 47.6661 65.6274 47.0488 66.2756C46.416 66.9238 46.1073 67.7572 46.1073 68.7758C46.1073 69.8099 46.4314 70.6433 47.0796 71.2915C47.7278 71.9397 48.5149 72.2638 49.4564 72.2638C50.6602 72.2638 51.6942 71.7391 52.5277 70.6741L54.4877 72.2484Z\"\n          fill=\"currentColor\"\n        />\n        <path\n          d=\"M91.8834 72.9581C90.7413 74.0848 89.306 74.6404 87.5775 74.6404C85.8489 74.6404 84.4136 74.0693 83.2561 72.9427C82.114 71.8161 81.543 70.3962 81.543 68.6985C81.543 66.9854 82.114 65.5655 83.2561 64.4388C84.3982 63.3122 85.8335 62.7566 87.5775 62.7566C89.306 62.7566 90.7413 63.3122 91.8834 64.4388C93.0255 65.5655 93.5965 66.9854 93.5965 68.6985C93.5965 70.4116 93.0255 71.8315 91.8834 72.9581ZM85.2007 71.2296C85.8335 71.8778 86.6206 72.2019 87.5775 72.2019C88.5343 72.2019 89.3369 71.8778 89.9542 71.2141C90.5716 70.5505 90.8802 69.7171 90.8802 68.6985C90.8802 67.6799 90.5716 66.8465 89.9542 66.1983C89.3369 65.5501 88.5498 65.226 87.5775 65.226C86.6052 65.226 85.8026 65.5501 85.1853 66.1983C84.5679 66.8465 84.2593 67.6799 84.2593 68.6985C84.2593 69.7325 84.5679 70.5814 85.2007 71.2296Z\"\n          fill=\"currentColor\"\n        />\n        <path\n          d=\"M80.2153 67.8034V74.5478H77.252V68.3744C77.252 67.4175 77.0205 66.7076 76.5421 66.2137C76.0637 65.7199 75.4 65.4729 74.5666 65.4729C73.3937 65.4729 72.4522 66.0594 71.6342 66.8619V74.5478H68.9951V63.127H71.5879V64.3463C72.6529 63.59 73.8875 63.0344 75.3229 63.0344C76.8045 63.0344 77.9928 63.4511 78.888 64.3C79.7677 65.1488 80.2153 66.3063 80.2153 67.8034Z\"\n          fill=\"currentColor\"\n        />\n        <path\n          d=\"M118.706 63.0344L111.576 78.2364H108.983L111.453 73.0508L106.452 63.0344H109.184L112.795 70.2264L116.222 63.0344H118.706Z\"\n          fill=\"currentColor\"\n        />\n      </g>\n      <defs>\n        <clipPath id=\"clip0_16329_10582\">\n          <rect\n            fill=\"white\"\n            height=\"21.6995\"\n            transform=\"translate(17 57)\"\n            width=\"102\"\n          />\n        </clipPath>\n      </defs>\n    </svg>\n  );\n}\n\nfunction Canva() {\n  const id1 = React.useId();\n  return (\n    <svg\n      aria-hidden=\"true\"\n      fill=\"none\"\n      height=\"136\"\n      preserveAspectRatio=\"xMidYMid meet\"\n      viewBox=\"0 0 136 136\"\n      width=\"136\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <mask\n        height=\"25\"\n        id={id1}\n        maskUnits=\"userSpaceOnUse\"\n        style={{ maskType: \"luminance\" }}\n        width=\"76\"\n        x=\"30\"\n        y=\"56\"\n      >\n        <path\n          d=\"M105.335 72.5994C105.24 72.5994 105.05 72.6944 105.05 72.8844C104.292 75.0664 103.248 76.3949 102.393 76.3949C101.918 76.3949 101.73 75.8249 101.73 74.9714C101.73 72.7894 103.058 68.1405 103.722 66.0535C103.817 65.7685 103.817 65.5785 103.817 65.39C103.817 64.82 103.437 64.4415 102.678 64.4415C101.825 64.4415 100.781 64.8215 99.9261 66.4335C99.6411 65.01 98.5976 64.3465 97.2691 64.3465C95.6556 64.3465 94.1386 65.39 92.9051 67.0035C91.6716 68.6169 90.1531 69.1854 89.1096 68.9005C89.8681 66.9085 90.2481 65.485 90.2481 64.4415C90.2481 62.733 89.3946 61.6895 88.0661 61.6895C85.9791 61.6895 84.8406 63.6815 84.8406 65.77C84.8406 67.3835 85.5991 68.9954 87.1176 69.8504C85.7891 72.8859 83.7972 75.6379 83.1322 75.6379C82.1837 75.6379 81.8987 70.9889 81.9937 67.5734C81.9937 65.6765 82.1837 65.5815 82.1837 65.0115C82.1837 64.6315 81.9937 64.4415 81.1402 64.4415C79.1482 64.4415 78.4832 66.15 78.3882 68.142C78.3882 68.9005 78.1982 69.6604 78.0082 70.3239C77.1547 73.3594 75.4462 75.6379 74.3077 75.6379C73.7377 75.6379 73.6442 75.0679 73.6442 74.4044C73.6442 72.2224 74.8777 69.4704 74.8777 67.1935C74.8777 65.485 74.1192 64.4415 72.6957 64.4415C71.0822 64.4415 68.8053 66.4335 66.8133 70.1339C67.4768 67.287 67.7618 64.5365 65.7698 64.5365C65.2948 64.5365 64.9163 64.6315 64.5363 64.8215C64.2513 64.9165 64.0613 65.2015 64.1563 65.485C64.3463 68.5205 61.6893 76.2064 59.2223 76.2064C58.7473 76.2064 58.5588 75.7314 58.5588 74.9729C58.5588 72.7909 59.8873 68.2369 60.5508 66.055C60.6458 65.77 60.6458 65.58 60.6458 65.2965C60.6458 64.7265 60.2658 64.443 59.5073 64.443C58.6538 64.443 57.6103 64.823 56.7553 66.435C56.4703 65.0115 55.4268 64.348 54.0984 64.348C51.8214 64.348 49.3544 66.7199 48.3109 69.7554C46.8874 73.8359 43.8519 77.8199 39.8664 77.8199C36.261 77.8199 34.364 74.7844 34.364 70.0404C33.889 63.3045 38.918 57.7055 42.6184 57.7055C44.4204 57.7055 45.2754 58.844 45.2754 60.5525C45.2754 62.6395 44.1369 63.683 44.1369 64.443C44.1369 64.728 44.3269 64.918 44.7069 64.918C46.3204 64.918 48.2174 63.021 48.2174 60.364C48.2174 57.707 46.1304 56 42.4299 56C36.2625 55.9955 30 62.163 30 70.1325C30 76.4899 33.1305 80.2839 38.5394 80.2839C42.2399 80.2839 45.4654 77.4369 47.1739 74.0214C47.3639 76.7734 48.5974 78.2904 50.5894 78.2904C52.2978 78.2904 53.7198 77.2469 54.7633 75.4435C55.1433 77.3404 56.2818 78.1954 57.6103 78.1954C59.2238 78.1954 60.5523 77.1519 61.8793 75.2535C61.8793 76.7719 62.1643 78.1004 63.4928 78.1004C64.0628 78.1004 64.8213 78.0054 64.9163 77.4369C66.2448 71.8395 69.6602 67.2855 70.6087 67.2855C70.8937 67.2855 70.9887 67.5705 70.9887 67.949C70.9887 69.4675 69.9452 72.598 69.9452 74.59C69.9452 76.7719 70.8937 78.1954 72.7922 78.1954C74.8792 78.1954 77.0612 75.6334 78.3897 71.838C78.8647 75.3485 79.8132 78.1954 81.3317 78.1954C83.1336 78.1954 86.4556 74.305 88.4476 70.226C89.2061 70.321 90.4396 70.321 91.4831 69.4675C91.0081 70.701 90.7246 72.0295 90.7246 73.3579C90.7246 77.1534 92.5266 78.1969 94.1401 78.1969C95.8486 78.1969 97.2706 77.1534 98.3141 75.3499C98.694 76.9634 99.5475 78.1019 101.161 78.1019C103.723 78.1019 106 75.4449 106 73.3579C105.904 72.9794 105.619 72.5994 105.335 72.5994ZM52.1064 76.2049C51.0629 76.2049 50.6829 75.1614 50.6829 73.6429C50.6829 70.986 52.4849 66.432 54.4783 66.432C55.3318 66.432 55.6168 67.4755 55.6168 68.709C55.6168 71.3659 53.9098 76.2049 52.1064 76.2049ZM87.5911 68.1405C86.9276 67.382 86.7376 66.432 86.7376 65.4835C86.7376 64.345 87.1176 63.4915 87.5911 63.4915C88.0661 63.4915 88.2546 63.9665 88.2546 64.63C88.2561 65.6735 87.8761 67.287 87.5911 68.1405ZM95.6571 76.2049C94.6136 76.2049 94.2336 74.9714 94.2336 73.6429C94.2336 71.081 96.0356 66.432 98.0291 66.432C98.8826 66.432 99.1676 67.4755 99.1676 68.709C99.1676 71.3659 97.4591 76.2049 95.6571 76.2049Z\"\n          fill=\"white\"\n        />\n      </mask>\n      <g mask={`url(#${id1})`}>\n        <path\n          d=\"M106.187 54.666H29.7139V81.5163H106.187V54.666Z\"\n          fill=\"currentColor\"\n        />\n      </g>\n    </svg>\n  );\n}\n\nfunction Casetext() {\n  return (\n    <svg\n      aria-hidden=\"true\"\n      fill=\"none\"\n      height=\"136\"\n      preserveAspectRatio=\"xMidYMid meet\"\n      viewBox=\"0 0 136 136\"\n      width=\"136\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <g clipPath=\"url(#clip0_16329_10575)\">\n        <path\n          d=\"M30.5608 66.5828C30.264 66.1113 29.8492 65.7123 29.3166 65.3859C28.784 65.0595 28.1772 64.8962 27.4961 64.8962C26.7802 64.8781 26.1384 64.996 25.5709 65.2499C25.0033 65.5038 24.5187 65.8574 24.1171 66.3108C23.7155 66.7641 23.4099 67.3036 23.2003 67.9293C22.9908 68.5549 22.886 69.2305 22.886 69.9559C22.886 70.6813 22.9908 71.3568 23.2003 71.9824C23.4099 72.6081 23.7155 73.1476 24.1171 73.601C24.5187 74.0543 25.0033 74.408 25.5709 74.6619C26.1384 74.9157 26.7802 75.0336 27.4961 75.0155C28.1772 75.0155 28.784 74.8523 29.3166 74.5259C29.8492 74.1994 30.264 73.8005 30.5608 73.329L31.9753 74.4442C31.3641 75.1878 30.6743 75.7363 29.906 76.09C29.1376 76.4436 28.3343 76.6295 27.4961 76.6476C26.5008 76.6658 25.6014 76.5071 24.7981 76.1716C23.9949 75.8361 23.3094 75.3691 22.7419 74.7707C22.1744 74.1722 21.7422 73.4604 21.4453 72.6353C21.1484 71.8102 21 70.917 21 69.9559C21 68.9947 21.1484 68.1016 21.4453 67.2764C21.7422 66.4513 22.1744 65.7395 22.7419 65.1411C23.3094 64.5426 23.9949 64.0756 24.7981 63.7401C25.6014 63.4046 26.5008 63.246 27.4961 63.2641C28.3343 63.2822 29.1376 63.4681 29.906 63.8217C30.6743 64.1754 31.3641 64.7239 31.9753 65.4675L30.5608 66.5828Z\"\n          fill=\"currentColor\"\n        />\n        <path\n          d=\"M42.4176 68.5142V68.1878C42.4176 65.9935 41.3698 64.8963 39.2743 64.8963C37.8423 64.8963 36.5937 65.395 35.5285 66.3924L34.4808 65.1139C35.6333 63.8807 37.3621 63.2642 39.6672 63.2642C40.2608 63.2642 40.8327 63.3548 41.3828 63.5362C41.9329 63.7175 42.4089 63.9941 42.8105 64.3659C43.2121 64.7376 43.5352 65.2046 43.7796 65.7668C44.0242 66.329 44.1464 66.9999 44.1464 67.7798V73.465C44.1464 73.9547 44.1682 74.467 44.2118 75.002C44.2555 75.5369 44.3036 75.9767 44.3558 76.3213H42.6795C42.6272 76.013 42.5879 75.6775 42.5616 75.3148C42.5354 74.9521 42.5223 74.5985 42.5223 74.2539H42.47C41.9635 75.1062 41.3655 75.7183 40.6756 76.09C39.9858 76.4618 39.1433 76.6477 38.1479 76.6477C37.6066 76.6477 37.0827 76.5706 36.5763 76.4165C36.0699 76.2623 35.6202 76.0266 35.2273 75.7092C34.8344 75.3919 34.52 75.002 34.2843 74.5395C34.0485 74.0771 33.9307 73.5376 33.9307 72.921C33.9307 71.8873 34.1882 71.0758 34.7034 70.4864C35.2185 69.897 35.8647 69.4572 36.6418 69.1671C37.4189 68.8769 38.2483 68.6956 39.1302 68.623C40.0121 68.5505 40.8198 68.5142 41.5531 68.5142H42.4176ZM41.527 69.9831C41.0905 69.9831 40.5447 70.0058 39.8899 70.0511C39.235 70.0965 38.6063 70.2053 38.0039 70.3776C37.4014 70.5499 36.8862 70.8173 36.4584 71.18C36.0306 71.5427 35.8166 72.0414 35.8166 72.6762C35.8166 73.0933 35.8996 73.4514 36.0655 73.7506C36.2314 74.0499 36.454 74.2947 36.7334 74.4851C37.0128 74.6755 37.3228 74.8116 37.6633 74.8931C38.0039 74.9748 38.3487 75.0156 38.698 75.0156C39.3267 75.0156 39.8724 74.9068 40.3351 74.6891C40.7979 74.4715 41.1864 74.1768 41.5007 73.8051C41.815 73.4333 42.0465 72.9981 42.195 72.4994C42.3433 72.0006 42.4176 71.4702 42.4176 70.908V69.9831H41.527Z\"\n          fill=\"currentColor\"\n        />\n        <path\n          d=\"M54.1271 66.6101C53.8652 66.0841 53.5159 65.667 53.0794 65.3587C52.6429 65.0505 52.1103 64.8963 51.4816 64.8963C51.1847 64.8963 50.8834 64.9326 50.5779 65.0051C50.2722 65.0776 49.9971 65.191 49.7527 65.3452C49.5081 65.4993 49.3117 65.6942 49.1634 65.93C49.0148 66.1658 48.9408 66.4559 48.9408 66.8005C48.9408 67.3989 49.1415 67.8342 49.5431 68.1062C49.9449 68.3782 50.5472 68.6139 51.3505 68.8134L53.1055 69.2487C53.9613 69.4482 54.6729 69.8517 55.2405 70.4592C55.8078 71.0667 56.0917 71.8238 56.0917 72.7306C56.0917 73.4197 55.9564 74.0136 55.6856 74.5123C55.4149 75.011 55.057 75.4191 54.6118 75.7364C54.1664 76.0538 53.6556 76.285 53.0794 76.4301C52.5032 76.5752 51.9181 76.6477 51.3244 76.6477C50.3814 76.6477 49.5038 76.4618 48.6919 76.09C47.8799 75.7183 47.1856 75.079 46.6094 74.1723L48.1025 73.1114C48.4518 73.6917 48.8926 74.1542 49.4252 74.4987C49.9578 74.8433 50.5908 75.0156 51.3244 75.0156C51.6735 75.0156 52.0228 74.9748 52.3721 74.8931C52.7214 74.8116 53.0313 74.6846 53.302 74.5123C53.5727 74.34 53.791 74.1179 53.9568 73.8459C54.1228 73.5738 54.2057 73.2565 54.2057 72.8938C54.2057 72.2591 53.9786 71.8012 53.5246 71.5201C53.0706 71.239 52.5205 71.0168 51.8745 70.8536L50.1979 70.4456C49.9885 70.3912 49.7046 70.3005 49.3467 70.1736C48.9887 70.0466 48.6394 69.8562 48.2989 69.6023C47.9585 69.3484 47.6659 69.0174 47.4213 68.6094C47.1769 68.2014 47.0547 67.6981 47.0547 67.0997C47.0547 66.4468 47.1813 65.8756 47.4345 65.3859C47.6877 64.8963 48.0282 64.4973 48.4561 64.189C48.884 63.8807 49.3685 63.6495 49.9099 63.4954C50.4512 63.3412 51.0101 63.2642 51.5863 63.2642C52.4419 63.2642 53.2366 63.4364 53.9699 63.781C54.7033 64.1256 55.2709 64.715 55.6727 65.5492L54.1271 66.6101Z\"\n          fill=\"currentColor\"\n        />\n        <path\n          d=\"M59.9947 70.4456C60.0472 71.0803 60.1955 71.6787 60.4401 72.2409C60.6845 72.8031 61.0075 73.2882 61.4092 73.6962C61.8108 74.1043 62.2736 74.4262 62.7974 74.6619C63.3214 74.8977 63.8889 75.0156 64.5001 75.0156C65.4256 75.0156 66.2245 74.7934 66.8967 74.3491C67.5692 73.9048 68.08 73.3925 68.4291 72.8122L69.7651 73.9547C69.0317 74.9158 68.224 75.6049 67.3421 76.022C66.4602 76.4391 65.5129 76.6477 64.5001 76.6477C63.5921 76.6477 62.7495 76.48 61.9723 76.1445C61.1953 75.809 60.5273 75.342 59.9686 74.7435C59.4096 74.1451 58.9688 73.4378 58.6456 72.6218C58.3226 71.8057 58.1611 70.9171 58.1611 69.9559C58.1611 68.9948 58.3183 68.1062 58.6326 67.2901C58.947 66.474 59.3835 65.7668 59.9422 65.1683C60.5012 64.5699 61.156 64.1029 61.9068 63.7674C62.6577 63.4319 63.4697 63.2642 64.3429 63.2642C65.2685 63.2642 66.0979 63.4364 66.8313 63.781C67.5647 64.1256 68.1804 64.5835 68.678 65.1547C69.1757 65.726 69.5556 66.397 69.8175 67.1677C70.0794 67.9384 70.2104 68.759 70.2104 69.6295V70.4456H59.9947ZM68.3244 68.9767C68.3244 67.7616 67.9751 66.7778 67.2767 66.0252C66.5781 65.2726 65.6003 64.8963 64.3429 64.8963C63.784 64.8963 63.2428 65.0096 62.7188 65.2363C62.1951 65.463 61.7365 65.7668 61.3436 66.1476C60.9509 66.5284 60.6365 66.9637 60.4008 67.4533C60.165 67.943 60.0472 68.4507 60.0472 68.9767H68.3244Z\"\n          fill=\"currentColor\"\n        />\n        <path\n          d=\"M79.4744 65.2228H75.9383V72.7307C75.9383 73.2022 75.9819 73.5875 76.0692 73.8868C76.1566 74.186 76.2788 74.4172 76.436 74.5804C76.5931 74.7437 76.7808 74.857 76.999 74.9205C77.2173 74.9839 77.4576 75.0157 77.7194 75.0157C78.0163 75.0157 78.322 74.9703 78.6363 74.8797C78.9505 74.789 79.2387 74.6711 79.5006 74.526L79.5791 76.1854C78.9332 76.4937 78.156 76.6478 77.2479 76.6478C76.9161 76.6478 76.5713 76.6025 76.2132 76.5118C75.8552 76.4211 75.5279 76.2488 75.2311 75.9949C74.9342 75.7411 74.6896 75.3965 74.4975 74.9613C74.3056 74.526 74.2095 73.9548 74.2095 73.2475V65.2228H71.6162V63.5907H74.2095V60H75.9383V63.5907H79.4744V65.2228Z\"\n          fill=\"currentColor\"\n        />\n        <path\n          d=\"M82.7291 70.4456C82.7815 71.0803 82.9299 71.6787 83.1744 72.2409C83.4188 72.8031 83.7418 73.2882 84.1436 73.6962C84.5451 74.1043 85.008 74.4262 85.5318 74.6619C86.0557 74.8977 86.6233 75.0156 87.2345 75.0156C88.16 75.0156 88.9588 74.7934 89.6313 74.3491C90.3036 73.9048 90.8144 73.3925 91.1635 72.8122L92.4994 73.9547C91.766 74.9158 90.9584 75.6049 90.0765 76.022C89.1946 76.4391 88.2472 76.6477 87.2345 76.6477C86.3264 76.6477 85.4838 76.48 84.7066 76.1445C83.9296 75.809 83.2617 75.342 82.7029 74.7435C82.144 74.1451 81.7032 73.4378 81.38 72.6218C81.057 71.8057 80.8955 70.9171 80.8955 69.9559C80.8955 68.9948 81.0527 68.1062 81.367 67.2901C81.6813 66.474 82.1179 65.7668 82.6766 65.1683C83.2355 64.5699 83.8903 64.1029 84.6412 63.7674C85.3921 63.4319 86.2041 63.2642 87.0773 63.2642C88.0028 63.2642 88.8323 63.4364 89.5657 63.781C90.2991 64.1256 90.9148 64.5835 91.4124 65.1547C91.91 65.726 92.29 66.397 92.5519 67.1677C92.8138 67.9384 92.9448 68.759 92.9448 69.6295V70.4456H82.7291ZM91.0588 68.9767C91.0588 67.7616 90.7095 66.7778 90.0111 66.0252C89.3125 65.2726 88.3347 64.8963 87.0773 64.8963C86.5184 64.8963 85.9771 65.0096 85.4532 65.2363C84.9294 65.463 84.4709 65.7668 84.0782 66.1476C83.6852 66.5284 83.3709 66.9637 83.1352 67.4533C82.8994 67.943 82.7815 68.4507 82.7815 68.9767H91.0588Z\"\n          fill=\"currentColor\"\n        />\n        <path\n          d=\"M98.577 69.5209L94.124 63.5908H96.4815L99.8868 68.3512L103.135 63.5908H105.283L101.013 69.5209L106.147 76.3215H103.79L99.6772 70.745L95.7218 76.3215H93.5215L98.577 69.5209Z\"\n          fill=\"currentColor\"\n        />\n        <path\n          d=\"M114.895 65.2228H111.359V72.7307C111.359 73.2022 111.403 73.5875 111.49 73.8868C111.577 74.186 111.7 74.4172 111.857 74.5804C112.014 74.7437 112.202 74.857 112.42 74.9205C112.638 74.9839 112.878 75.0157 113.14 75.0157C113.437 75.0157 113.743 74.9703 114.057 74.8797C114.371 74.789 114.66 74.6711 114.921 74.526L115 76.1854C114.354 76.4937 113.577 76.6478 112.669 76.6478C112.337 76.6478 111.992 76.6025 111.634 76.5118C111.276 76.4211 110.949 76.2488 110.652 75.995C110.355 75.7411 110.111 75.3965 109.918 74.9613C109.726 74.526 109.63 73.9548 109.63 73.2475V65.2228H107.037V63.5907H109.63V60H111.359V63.5907H114.895V65.2228Z\"\n          fill=\"currentColor\"\n        />\n      </g>\n      <defs>\n        <clipPath id=\"clip0_16329_10575\">\n          <rect\n            fill=\"white\"\n            height=\"16.732\"\n            transform=\"translate(21 60)\"\n            width=\"94\"\n          />\n        </clipPath>\n      </defs>\n    </svg>\n  );\n}\n\nfunction _Classdojo() {\n  return (\n    <svg\n      aria-hidden=\"true\"\n      fill=\"none\"\n      height=\"136\"\n      preserveAspectRatio=\"xMidYMid meet\"\n      viewBox=\"0 0 136 136\"\n      width=\"136\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <g clipPath=\"url(#clip0_16329_10578)\">\n        <path\n          clipRule=\"evenodd\"\n          d=\"M123.65 63.7261C123.34 64.0354 122.921 64.2091 122.484 64.2091C122.267 64.2082 122.053 64.1645 121.853 64.0807C121.653 63.9969 121.472 63.8744 121.32 63.7205C121.167 63.5665 121.047 63.384 120.965 63.1834C120.884 62.9828 120.842 62.7681 120.844 62.5515C120.842 62.3357 120.884 62.1218 120.966 61.9222C121.048 61.7226 121.168 61.5413 121.321 61.3887C121.473 61.2361 121.655 61.1154 121.854 61.0334C122.054 60.9515 122.268 60.91 122.484 60.9114C122.921 60.9114 123.34 61.0851 123.65 61.3943C123.959 61.7036 124.133 62.1229 124.133 62.5602C124.133 62.9975 123.959 63.4169 123.65 63.7261ZM123.918 73.9345C123.918 76.1155 122.84 77.6614 120.435 77.6614C119.207 77.6614 118.76 77.4765 118.069 77.0263L118.799 74.9395C119.148 75.2396 119.409 75.3513 119.95 75.3513C120.624 75.3513 121.049 74.9046 121.049 73.9345V65.0117H123.918V73.9345ZM135.001 69.4993C135.001 72.0362 133.176 74.2346 130.08 74.2346H130.084C127.027 74.2346 125.202 72.0362 125.202 69.4993C125.202 66.9658 127.027 64.7883 130.08 64.7883C133.176 64.7883 135.001 66.9658 135.001 69.4993ZM128.147 69.4993C128.147 70.7102 128.856 71.6977 130.08 71.6977H130.084C131.351 71.6977 132.059 70.7102 132.059 69.4993C132.059 68.3093 131.351 67.3218 130.084 67.3218C128.852 67.3218 128.147 68.3093 128.147 69.4993ZM63.7434 70.8986C62.9442 72.4829 61.3251 74.2346 58.324 74.2346C54.5064 74.2346 51.5996 71.6244 51.5996 67.8068C51.5996 63.9683 54.5064 61.3755 58.324 61.3755C61.3216 61.3755 62.9408 63.0715 63.7434 64.6906L60.9866 66.0167C60.7615 65.493 60.3911 65.0449 59.9192 64.7251C59.4474 64.4054 58.8938 64.2275 58.324 64.2125C56.2931 64.2125 54.8589 65.7549 54.8589 67.8068C54.8589 69.8378 56.2931 71.4011 58.324 71.4011C58.8942 71.3858 59.448 71.2074 59.9199 70.887C60.3918 70.5667 60.762 70.1178 60.9866 69.5935L63.7434 70.8986ZM67.7075 61.5849V74.0078H64.8391V61.5849H67.7075ZM77.7436 68.3826V74.0078H74.8751V73.118C74.3168 73.7845 73.235 74.2346 72.0835 74.2346C70.7016 74.2346 68.9917 73.282 68.9917 71.272C68.9917 69.091 70.7051 68.3652 72.0835 68.3652C73.2734 68.3652 74.3377 68.756 74.8751 69.426V68.5327C74.8751 67.751 74.2051 67.2101 73.0501 67.2101C72.125 67.227 71.2354 67.5692 70.5376 68.1767L69.4733 66.1842C70.6283 65.1966 72.1742 64.7883 73.5561 64.7883C75.7161 64.7883 77.7436 65.5909 77.7436 68.3826ZM71.8392 71.3069C71.8392 72.0153 72.5301 72.3503 73.2769 72.3503H73.2734C73.9085 72.3503 74.5785 72.1269 74.8786 71.7012V70.8986C74.5785 70.4694 73.9085 70.2635 73.2734 70.2635C72.5301 70.2635 71.8392 70.5985 71.8392 71.3069ZM87.155 71.2894C87.155 73.0028 85.644 74.2346 83.0757 74.2346V74.2311C81.453 74.2311 79.7954 73.6937 78.8463 72.8353L80.0397 70.7869C80.7097 71.3836 82.1439 71.9978 83.2048 71.9978C84.0632 71.9978 84.4157 71.7535 84.4157 71.3627C84.4157 70.965 83.7518 70.8555 82.8969 70.7144C81.3513 70.4593 79.1813 70.1012 79.1813 67.751C79.1813 66.1667 80.5597 64.7883 83.0373 64.7883C84.3863 64.7714 85.7051 65.1886 86.799 65.9783L85.6998 68.0092C85.1589 67.4893 84.1365 67.0217 83.0547 67.0217C82.3847 67.0217 81.938 67.2834 81.938 67.6358C81.938 67.9726 82.5447 68.0756 83.3442 68.2114C84.8893 68.4739 87.155 68.8587 87.155 71.2894ZM95.9592 71.2894C95.9592 73.0028 94.4482 74.2346 91.8799 74.2346V74.2311C90.2572 74.2311 88.5997 73.6937 87.6505 72.8353L88.8439 70.7869C89.5139 71.3836 90.9481 71.9978 92.009 71.9978C92.8674 71.9978 93.2199 71.7535 93.2199 71.3627C93.2199 70.965 92.556 70.8555 91.7011 70.7144C90.1555 70.4593 87.9855 70.1012 87.9855 67.751C87.9855 66.1667 89.3639 64.7883 91.8415 64.7883C93.1906 64.7714 94.5094 65.1886 95.6033 65.9783L94.504 68.0092C93.9666 67.4893 92.9407 67.0217 91.8589 67.0217C91.1889 67.0217 90.7423 67.2834 90.7423 67.6358C90.7423 67.9726 91.3489 68.0756 92.1484 68.2114C93.6935 68.4739 95.9592 68.8587 95.9592 71.2894ZM109.234 67.7859C109.234 71.6419 106.421 74.0078 102.527 74.0078H97.2957V61.5814H102.509C106.421 61.5814 109.234 63.9299 109.234 67.7859ZM100.499 71.2161H102.509C104.69 71.2161 105.957 69.6319 105.957 67.7859C105.957 65.8666 104.802 64.3766 102.527 64.3766H100.499V71.2161ZM119.758 69.4993C119.758 72.0362 117.93 74.2346 114.838 74.2346C111.784 74.2346 109.959 72.0362 109.959 69.4993C109.959 66.9658 111.784 64.7883 114.838 64.7883C117.93 64.7883 119.758 66.9658 119.758 69.4993ZM112.901 69.4993C112.901 70.7102 113.61 71.6977 114.838 71.6977C116.105 71.6977 116.813 70.7102 116.813 69.4993C116.813 68.3093 116.105 67.3218 114.838 67.3218C113.61 67.3218 112.901 68.3093 112.901 69.4993Z\"\n          fill=\"currentColor\"\n          fillRule=\"evenodd\"\n        />\n        <path\n          d=\"M7.4554 51.8385C8.51539 51.8385 9.37467 50.9793 9.37467 49.9193C9.37467 48.8593 8.51539 48 7.4554 48C6.39542 48 5.53613 48.8593 5.53613 49.9193C5.53613 50.9793 6.39542 51.8385 7.4554 51.8385Z\"\n          fill=\"#797979\"\n        />\n        <mask\n          height=\"39\"\n          id=\"mask0_16329_10578\"\n          maskUnits=\"userSpaceOnUse\"\n          style={{ maskType: \"alpha\" }}\n          width=\"39\"\n          x=\"3\"\n          y=\"49\"\n        >\n          <path\n            d=\"M28.0435 86.5141C38.0842 83.2516 43.5791 72.4673 40.3167 62.4266C37.0542 52.3859 26.2699 46.891 16.2292 50.1534C6.18844 53.4158 0.693549 64.2002 3.95598 74.2409C7.21841 84.2816 18.0027 89.7765 28.0435 86.5141Z\"\n            fill=\"#00B2F7\"\n          />\n        </mask>\n        <g mask=\"url(#mask0_16329_10578)\">\n          <path\n            d=\"M-1.95996 56.0644L34.3945 44.2522L47.1559 83.5275L10.7945 95.3397L-1.95996 56.0644Z\"\n            fill=\"#E2E2E2\"\n          />\n          <mask\n            height=\"37\"\n            id=\"mask1_16329_10578\"\n            maskUnits=\"userSpaceOnUse\"\n            style={{ maskType: \"alpha\" }}\n            width=\"31\"\n            x=\"8\"\n            y=\"53\"\n          >\n            <path\n              d=\"M10.2394 59.3204C8.61327 61.3792 7.79671 63.1729 9.33212 67.5174C10.8745 71.8619 15.7774 87.3487 15.7774 87.3487L29.0448 89.3447L29.1041 89.5192L29.2472 89.3796L29.4496 89.4076L29.3937 89.2331L38.9552 79.8182C38.9552 79.8182 33.815 64.4047 32.5099 59.9869C31.2013 55.5691 29.481 54.599 26.958 53.8871C25.3353 53.4334 20.0137 54.3617 19.0715 54.6653L18.7086 54.7839L18.6807 54.7944L18.2236 54.9479L18.077 54.9968L17.9305 55.0456L17.4698 55.1852L17.4384 55.1957L17.0755 55.3143C16.1333 55.6214 11.2828 57.9978 10.2394 59.3204Z\"\n              fill=\"#83DC1F\"\n            />\n          </mask>\n          <g mask=\"url(#mask1_16329_10578)\">\n            <path\n              d=\"M5.18945 59.176L32.8615 50.1848L43.7258 83.6216L16.0537 92.6128L5.18945 59.176Z\"\n              fill=\"url(#paint0_linear_16329_10578)\"\n            />\n            <path\n              d=\"M10.2951 70.773C9.33197 69.5586 8.46655 65.9364 8.40723 64.9593C16.175 57.0449 27.6697 56.361 31.6688 57.4009C33.117 59.0759 33.8254 63.1273 33.5567 63.2145C22.1771 61.1976 13.1565 67.5067 10.2951 70.773Z\"\n              fill=\"#6A6A6A\"\n            />\n          </g>\n        </g>\n        <path\n          d=\"M11.0049 66.7878C10.4221 65.1233 9.02629 63.71 7.03723 63.0226C5.07451 62.3616 2.93205 62.4867 1.05957 63.3715C1.64233 65.0396 3.03816 66.4598 5.03769 67.1473C5.42504 67.2799 5.81936 67.3811 6.21717 67.4509C5.86473 67.6219 5.51926 67.8173 5.19124 68.0441C3.44645 69.234 2.47285 70.9719 2.3577 72.7376C4.31884 73.0796 6.46842 72.6817 8.21322 71.4918C9.95801 70.2984 10.9316 68.5605 11.0468 66.7948L11.0119 66.7878H11.0049Z\"\n          fill=\"#1D1D1D\"\n        />\n        <path\n          d=\"M17.3248 70.7873C17.8069 70.6306 18.0707 70.1129 17.9141 69.6308C17.7574 69.1488 17.2397 68.885 16.7576 69.0416C16.2756 69.1982 16.0117 69.716 16.1684 70.198C16.325 70.6801 16.8428 70.9439 17.3248 70.7873Z\"\n          fill=\"#1D1D1D\"\n        />\n        <path\n          d=\"M28.0524 67.3083C28.5344 67.1516 28.7982 66.6339 28.6416 66.1518C28.485 65.6698 27.9672 65.406 27.4851 65.5626C27.0031 65.7192 26.7393 66.237 26.8959 66.719C27.0525 67.2011 27.5703 67.4649 28.0524 67.3083Z\"\n          fill=\"#1D1D1D\"\n        />\n        <mask\n          height=\"11\"\n          id=\"mask2_16329_10578\"\n          maskUnits=\"userSpaceOnUse\"\n          style={{ maskType: \"alpha\" }}\n          width=\"16\"\n          x=\"16\"\n          y=\"69\"\n        >\n          <path\n            d=\"M23.54 72.4339C22.9817 72.6153 21.4532 72.9433 20.2947 72.7933C18.8465 72.6083 16.3968 72.905 16.5434 75.2465C16.6865 77.5915 19.7119 81.1997 25.9548 79.2874L26.0944 79.2385C32.2744 77.1169 32.6025 72.4269 31.3392 70.4413C30.083 68.4592 27.9264 69.6596 26.8621 70.6611C26.0106 71.4638 24.5904 72.0919 24.032 72.2768L23.5435 72.4339H23.54Z\"\n            fill=\"#2C2A50\"\n          />\n        </mask>\n        <g mask=\"url(#mask2_16329_10578)\">\n          <path\n            d=\"M15.9355 73.6693L30.9373 68.7979L33.544 76.8239L18.5423 81.6988L15.9355 73.6693Z\"\n            fill=\"#1D1D1D\"\n          />\n          <path\n            d=\"M19.0479 79.4481C19.5272 78.2535 20.2633 77.1789 21.2041 76.3003C22.1448 75.4217 23.2671 74.7606 24.4916 74.3638C25.7153 73.9654 27.0116 73.8408 28.2888 73.9988C29.5659 74.1568 30.7928 74.5935 31.8825 75.2781C31.4031 76.4726 30.667 77.5473 29.7263 78.4259C28.7856 79.3045 27.6633 79.9656 26.4388 80.3624C25.2151 80.7607 23.9188 80.8853 22.6416 80.7274C21.3644 80.5694 20.1376 80.1326 19.0479 79.4481Z\"\n            fill=\"#BABABA\"\n          />\n          <path\n            d=\"M30.1454 77.1099L30.1559 77.1204L30.1768 77.0855L30.2745 77.0227L30.2431 76.9878L32.0402 74.381C31.3109 74.0042 29.7092 73.4493 29.1125 74.2415C28.6937 74.7963 29.0985 75.6931 29.5696 76.3876C28.945 75.9095 28.1424 75.5221 27.5736 75.892C26.9594 76.2898 27.0397 77.2879 27.2525 78.0835C26.8338 77.41 26.2161 76.733 25.3367 77.0192C24.4225 77.3158 24.3527 78.2929 24.4225 79.1059C24.1363 78.3138 23.6024 77.3891 22.8417 77.4309C22.1473 77.4658 21.725 78.2859 21.5017 79.0501C21.4842 78.2021 21.2853 77.2111 20.6118 77.0052C19.6627 76.7156 18.6926 78.1044 18.3262 78.8372L21.3272 79.8981L21.3202 79.933L21.411 79.9295L21.4598 79.9469V79.926L24.5062 79.7655L24.5167 79.8318L27.6573 78.8093L27.6224 78.7465L30.1454 77.1064V77.1099Z\"\n            fill=\"#F5F5F5\"\n          />\n        </g>\n        <path\n          d=\"M11.9193 49.9404C13.8491 49.5321 18.396 52.8367 19.8756 54.6024L12.9976 57.7082C13.1477 56.9544 13.1023 56.773 12.3974 54.6024C11.9333 53.1647 8.75079 50.6104 11.9228 49.9369L11.9193 49.9404Z\"\n          fill=\"#797979\"\n        />\n        <path\n          d=\"M9.9329 69.6215C8.96629 68.4001 8.40796 64.6768 8.34863 63.6962C16.1095 55.7818 26.9691 55.3072 30.9646 56.3506C32.4128 58.0256 33.4352 61.9863 33.17 62.0735C21.794 60.0426 12.7839 66.3518 9.92941 69.6215H9.9329Z\"\n          fill=\"#1D1D1D\"\n        />\n        <mask\n          height=\"13\"\n          id=\"mask3_16329_10578\"\n          maskUnits=\"userSpaceOnUse\"\n          style={{ maskType: \"alpha\" }}\n          width=\"11\"\n          x=\"34\"\n          y=\"63\"\n        >\n          <path\n            d=\"M43.8868 65.5178C42.9551 64.593 40.9765 64.2196 40.2681 64.3906C39.7342 61.3093 38.7851 64.2441 38.1325 65.1479C37.145 65.5666 34.793 67.3882 34.793 67.3882L37.4346 75.4666C37.4346 75.4666 40.2716 68.4769 43.7298 66.268C43.9985 66.0935 44.1136 65.7446 43.8868 65.5178Z\"\n            fill=\"#83DC1F\"\n          />\n        </mask>\n        <g mask=\"url(#mask3_16329_10578)\">\n          <path\n            d=\"M33.917 64.8024L43.632 61.6443L47.1914 72.612L37.4799 75.7666L33.917 64.8024Z\"\n            fill=\"#767676\"\n          />\n          <path\n            d=\"M39.4517 72.3222C36.7577 69.9388 35.8365 67.4996 35.7143 66.5784L34.5732 66.8226L37.7313 76.419L39.4517 72.3187V72.3222Z\"\n            fill=\"#6F6F6F\"\n          />\n        </g>\n        <mask\n          height=\"10\"\n          id=\"mask4_16329_10578\"\n          maskUnits=\"userSpaceOnUse\"\n          style={{ maskType: \"alpha\" }}\n          width=\"12\"\n          x=\"3\"\n          y=\"74\"\n        >\n          <path\n            d=\"M3.72836 78.935C3.93773 77.6787 5.27424 76.2585 5.92679 75.9898C4.58679 73.2574 7.01554 75.0267 8.04497 75.3652C9.06393 75.1313 11.9603 75.2186 11.9603 75.2186L14.5042 83.0946C14.5042 83.0946 8.28226 79.2211 4.29367 79.448C3.97612 79.4654 3.67252 79.2491 3.72836 78.935Z\"\n            fill=\"#83DC1F\"\n          />\n        </mask>\n        <g mask=\"url(#mask4_16329_10578)\">\n          <path\n            d=\"M1.93164 75.3582L11.0325 72.406L14.5011 83.0841L5.3968 86.0363L1.93164 75.3582Z\"\n            fill=\"#767676\"\n          />\n          <path\n            d=\"M11.1369 81.7581C11.8976 78.3418 11.2276 75.8956 10.7949 75.0965L11.8348 74.6428L14.8359 84.0019L11.1369 81.7581Z\"\n            fill=\"#6F6F6F\"\n          />\n        </g>\n      </g>\n      <defs>\n        <linearGradient\n          gradientUnits=\"userSpaceOnUse\"\n          id=\"paint0_linear_16329_10578\"\n          x1=\"38.5909\"\n          x2=\"27.5454\"\n          y1=\"82.0465\"\n          y2=\"60.564\"\n        >\n          <stop stopColor=\"#6F6F6F\" />\n          <stop offset=\"1\" stopColor=\"#787878\" />\n        </linearGradient>\n        <clipPath id=\"clip0_16329_10578\">\n          <rect\n            fill=\"white\"\n            height=\"39.7812\"\n            transform=\"translate(1 48)\"\n            width=\"134\"\n          />\n        </clipPath>\n      </defs>\n    </svg>\n  );\n}\n\nfunction Clearbit() {\n  return (\n    <svg\n      aria-hidden=\"true\"\n      fill=\"none\"\n      height=\"136\"\n      preserveAspectRatio=\"xMidYMid meet\"\n      viewBox=\"0 0 136 136\"\n      width=\"136\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <path\n        d=\"M40.6612 67.8929V75.297C40.6612 78.2507 40.3461 79.3141 39.775 80.3971C39.204 81.4802 38.3572 82.3269 37.2742 82.898C36.1912 83.469 35.1278 83.7841 32.1938 83.7841H24.8291V67.8929H40.6612Z\"\n        fill=\"currentColor\"\n        fillOpacity=\"0.2\"\n      />\n      <path\n        d=\"M24.8291 52H32.1938C35.1278 52 36.1912 52.3151 37.2742 52.8861C38.3572 53.4572 39.1843 54.3039 39.775 55.387C40.3461 56.47 40.6612 57.5333 40.6612 60.4871V67.8911H24.8291V52Z\"\n        fill=\"currentColor\"\n        fillOpacity=\"0.6\"\n      />\n      <path\n        d=\"M17.4674 52H24.8321V83.7823H17.4674C14.5333 83.7823 13.47 83.4672 12.387 82.8961C11.3039 82.3251 10.4769 81.4783 9.88612 80.3953C9.31507 79.3123 9 78.2489 9 75.2952V60.4871C9 57.5333 9.31507 56.47 9.88612 55.387C10.4572 54.3039 11.3039 53.4572 12.387 52.8861C13.47 52.3151 14.5333 52 17.4674 52Z\"\n        fill=\"currentColor\"\n      />\n      <path\n        d=\"M67.6831 66.2417C67.3376 65.7748 66.8577 65.42 66.3011 65.2332C65.7253 65.0091 65.1687 64.897 64.6313 64.897C63.9403 64.897 63.3069 65.0278 62.7311 65.2706C62.1744 65.5134 61.6754 65.8495 61.2531 66.2791C60.8501 66.7273 60.5238 67.2316 60.3126 67.7919C60.1015 68.3895 59.9863 69.0432 59.9863 69.6782C59.9863 70.3879 60.1015 71.0416 60.3126 71.6392C60.5046 72.1995 60.8117 72.7225 61.2147 73.1707C61.6178 73.6003 62.1169 73.9178 62.6543 74.1606C63.2109 74.4033 63.8443 74.5154 64.5353 74.5154C65.2647 74.5154 65.8981 74.3847 66.4547 74.1045C67.0113 73.8244 67.4528 73.4508 67.7982 73.0026L69.7176 74.31C69.1418 75.0383 68.3933 75.6173 67.5487 76.0095C66.685 76.4017 65.6869 76.6072 64.5353 76.6072C63.4796 76.6072 62.5199 76.4391 61.637 76.1029C60.7925 75.7854 60.0247 75.2998 59.3913 74.6835C58.7579 74.0672 58.2589 73.3201 57.9134 72.4983C57.5679 71.6392 57.376 70.7054 57.376 69.6782C57.376 68.6323 57.5679 67.6798 57.9326 66.8394C58.3165 65.9803 58.8155 65.2706 59.4681 64.6729C60.1399 64.0753 60.9268 63.6084 61.7714 63.3095C62.6543 62.992 63.614 62.824 64.6505 62.824C65.0727 62.824 65.5334 62.8613 65.994 62.9547C66.4547 63.0294 66.9153 63.1601 67.3376 63.3282C67.7599 63.4963 68.1629 63.6831 68.5468 63.9445C68.9307 64.1873 69.257 64.4862 69.5065 64.841L67.6831 66.2417ZM71.1188 62.2823H73.3837V76.271H71.1188V62.2823ZM82.5583 70.9295C82.5583 70.6494 82.5199 70.3692 82.424 70.1078C82.3472 69.8463 82.2128 69.6222 82.0401 69.4167C81.8482 69.2113 81.6178 69.0619 81.3491 68.9498C81.0804 68.8191 80.7733 68.7631 80.4086 68.7631C79.7176 68.7631 79.1418 68.9685 78.662 69.3794C78.2013 69.7716 77.9326 70.2945 77.8942 70.9109C77.8942 70.9295 82.5583 70.9295 82.5583 70.9295ZM84.8232 71.9381V72.2369C84.8232 72.3303 84.8232 72.4423 84.804 72.5357H77.8942C77.9134 72.8532 78.0094 73.152 78.1437 73.4322C78.2973 73.6936 78.4892 73.9178 78.7388 74.1232C78.9691 74.31 79.257 74.4594 79.5449 74.5714C79.8328 74.6835 80.1591 74.7395 80.4854 74.7395C81.0612 74.7395 81.5411 74.6461 81.9249 74.4407C82.3088 74.2353 82.6351 73.9364 82.8846 73.5816L84.4009 74.7582C83.4988 75.9348 82.2128 76.5325 80.5046 76.5325C79.7944 76.5325 79.1418 76.4204 78.5468 76.215C77.971 76.0095 77.4528 75.692 76.9921 75.2811C76.5507 74.8703 76.2052 74.3847 75.9748 73.843C75.7445 73.2641 75.6101 72.6104 75.6101 71.882C75.6101 71.1723 75.7253 70.5186 75.9748 69.9397C76.2244 69.342 76.5698 68.8564 76.9921 68.4456C77.4144 68.0347 77.9326 67.6985 78.5084 67.4744C79.1226 67.2316 79.7752 67.1195 80.4278 67.1195C81.0612 67.1195 81.637 67.2316 82.1745 67.437C82.7119 67.6238 83.1917 67.9413 83.5756 68.3522C83.9595 68.7444 84.2666 69.2487 84.4777 69.8463C84.708 70.4439 84.8232 71.135 84.8232 71.9381ZM92.5583 75.1317H92.5008C92.2704 75.5239 91.9057 75.8601 91.4067 76.1216C90.9077 76.3644 90.3318 76.4951 89.6793 76.4951C89.3146 76.4951 88.9307 76.4391 88.5276 76.3457C88.1437 76.2523 87.7599 76.1029 87.4336 75.8975C87.1073 75.6733 86.8194 75.3745 86.6082 75.0383C86.3971 74.6835 86.2819 74.2353 86.2819 73.731C86.2819 73.0586 86.4739 72.5357 86.8578 72.1435C87.2416 71.7513 87.7407 71.4525 88.3549 71.2283C88.9691 71.0229 89.6217 70.8922 90.3702 70.8175C91.0996 70.7428 91.829 70.7054 92.5391 70.7054V70.4813C92.5391 69.921 92.328 69.5101 91.9057 69.2673C91.5027 68.9872 91.0228 68.8564 90.447 68.8564C89.9672 68.8564 89.5065 68.9498 89.065 69.1553C88.6236 69.3607 88.2589 69.5848 87.971 69.8837L86.8002 68.5389C87.3184 68.072 87.9134 67.7172 88.5852 67.4744C89.257 67.2503 89.948 67.1195 90.6581 67.1195C91.4643 67.1195 92.1361 67.2316 92.6543 67.4557C93.1917 67.6798 93.5948 67.96 93.9019 68.3335C94.209 68.6884 94.4201 69.0992 94.5353 69.5288C94.6697 69.977 94.7272 70.4253 94.7272 70.8548V76.2523H92.5967L92.5583 75.1317ZM92.5199 72.2182H92.0017C91.637 72.2182 91.2531 72.2369 90.8501 72.2742C90.4662 72.2929 90.1015 72.3676 89.7368 72.4797C89.3913 72.5731 89.1034 72.7225 88.8923 72.9279C88.662 73.1147 88.5468 73.3761 88.5468 73.7123C88.5468 73.9178 88.5852 74.1045 88.6812 74.2539C88.7771 74.3847 88.9115 74.4967 89.065 74.5901C89.2186 74.6835 89.3913 74.7395 89.5833 74.7769C89.7752 74.8142 89.9672 74.8329 90.1591 74.8329C90.9461 74.8329 91.5411 74.6275 91.9441 74.2166C92.3472 73.8057 92.5583 73.2454 92.5583 72.5544V72.2182H92.5199ZM97.3184 67.381H99.4873V68.8564H99.5257C99.756 68.3522 100.121 67.9226 100.581 67.6051C101.042 67.2876 101.56 67.1195 102.174 67.1195C102.27 67.1195 102.366 67.1195 102.462 67.1382C102.558 67.1382 102.654 67.1569 102.731 67.1756V69.2113C102.597 69.1739 102.462 69.1553 102.328 69.1366C102.232 69.1179 102.117 69.1179 102.021 69.1179C101.503 69.1179 101.08 69.2113 100.773 69.3981C100.485 69.5662 100.236 69.7903 100.044 70.0704C99.8712 70.3132 99.756 70.5747 99.6793 70.8735C99.6217 71.1536 99.5833 71.3591 99.5833 71.5272V76.2897H97.3184C97.3184 76.271 97.3184 67.381 97.3184 67.381ZM106.531 62.2823V68.5203H106.589C106.704 68.3709 106.858 68.2214 107.031 68.0534C107.203 67.8853 107.414 67.7359 107.664 67.6051C107.933 67.4557 108.24 67.3437 108.528 67.269C108.892 67.1569 109.276 67.1195 109.641 67.1195C110.293 67.1195 110.869 67.2503 111.426 67.4931C111.963 67.7359 112.443 68.0534 112.827 68.4642C113.23 68.8938 113.537 69.3981 113.729 69.9397C113.94 70.5186 114.055 71.1536 114.055 71.77C114.055 72.405 113.959 73.0213 113.729 73.6189C113.537 74.1792 113.23 74.6835 112.846 75.113C112.462 75.5426 111.983 75.8788 111.445 76.1216C110.908 76.3644 110.274 76.4951 109.583 76.4951C108.95 76.4951 108.336 76.3644 107.76 76.0842C107.222 75.8227 106.762 75.4119 106.455 74.9076H106.416V76.2336H104.267V62.245H106.531V62.2823ZM111.752 71.7886C111.752 71.4525 111.695 71.0976 111.579 70.7801C111.483 70.4439 111.33 70.1264 111.1 69.8463C110.888 69.5661 110.601 69.342 110.293 69.1926C109.967 69.0245 109.583 68.9312 109.142 68.9312C108.72 68.9312 108.355 69.0245 108.029 69.1926C107.702 69.3607 107.414 69.5848 107.184 69.865C106.954 70.1451 106.781 70.4626 106.647 70.7988C106.531 71.135 106.474 71.4898 106.474 71.826C106.474 72.1622 106.531 72.517 106.647 72.8532C106.781 73.1894 106.954 73.4882 107.184 73.7683C107.414 74.0485 107.702 74.2539 108.029 74.4407C108.355 74.6088 108.739 74.6835 109.142 74.6835C109.583 74.6835 109.967 74.5901 110.293 74.422C110.62 74.2539 110.888 74.0298 111.1 73.7497C111.311 73.4695 111.464 73.1707 111.579 72.8158C111.695 72.4983 111.752 72.1435 111.752 71.7886ZM116.282 67.381H118.547V76.271H116.282V67.381ZM115.956 64.3741C115.956 64.0193 116.09 63.7018 116.359 63.4403C116.627 63.1601 116.992 63.0294 117.395 63.0294C117.817 63.0294 118.163 63.1601 118.432 63.4216C118.72 63.6644 118.873 63.9819 118.873 64.3741C118.873 64.7476 118.72 65.0838 118.432 65.3453C118.163 65.5881 117.798 65.7188 117.395 65.7188C116.973 65.7188 116.627 65.5881 116.359 65.3266C116.109 65.0651 115.956 64.729 115.956 64.3741ZM120.006 69.1553V67.381H121.599V64.8037H123.825V67.381H126.109V69.1553H123.844V73.2828C123.844 73.675 123.921 74.0111 124.055 74.2726C124.209 74.5341 124.535 74.6648 125.034 74.6648C125.188 74.6648 125.341 74.6461 125.533 74.6275C125.706 74.5901 125.86 74.5341 126.013 74.4781L126.09 76.215C125.86 76.2897 125.61 76.3457 125.361 76.383C125.092 76.4391 124.823 76.4577 124.574 76.4577C123.959 76.4577 123.48 76.383 123.096 76.215C122.712 76.0469 122.405 75.8227 122.194 75.5239C121.983 75.2251 121.829 74.8889 121.733 74.5341C121.656 74.1232 121.618 73.731 121.618 73.3201V69.1739H120.006V69.1553Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n}\n\nfunction Descript() {\n  return (\n    <svg\n      aria-hidden=\"true\"\n      fill=\"none\"\n      height=\"136\"\n      preserveAspectRatio=\"xMidYMid meet\"\n      viewBox=\"0 0 136 136\"\n      width=\"136\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <path\n        d=\"M15 78.6357C15 79.9955 15.8789 80.8774 17.2337 80.8774L23.7599 80.8775C27.6531 80.8775 30.8378 79.5554 33.0031 77.2409H15V78.6357ZM23.7599 56.0001L17.2337 56C15.8789 56 15 56.882 15 58.2418V59.6366H33.0031C30.8378 57.3221 27.6531 56.0001 23.7599 56.0001ZM29.4553 71.9835C29.4553 73.0835 30.1693 73.7973 31.2701 73.7973H35.1807C35.624 72.6932 35.9219 71.4794 36.0603 70.1697H31.2701C30.1693 70.1697 29.4553 70.8833 29.4553 71.9835ZM24.0349 64.903C24.0349 66.0032 24.7489 66.7168 25.8497 66.7168H36.0603C35.9219 65.407 35.624 64.1932 35.1807 63.0892H25.8497C24.7489 63.0892 24.0349 63.8028 24.0349 64.903ZM25.8485 71.9835C25.8485 70.8833 25.1345 70.1697 24.0336 70.1697H15V73.7973H24.0336C25.1345 73.7973 25.8485 73.0837 25.8485 71.9835ZM20.4281 64.903C20.4281 63.8028 19.7141 63.0892 18.6132 63.0892H15V66.7168H18.6132C19.7141 66.7168 20.4281 66.0032 20.4281 64.903Z\"\n        fill=\"currentColor\"\n      />\n      <path\n        d=\"M50.3072 72.1704C48.3729 72.1704 47.2101 70.7781 47.2101 69.0607C47.2101 67.3433 48.3952 65.9511 50.3072 65.9511C52.2192 65.9511 53.4043 67.3433 53.4043 69.0607C53.4043 70.7781 52.2415 72.1704 50.3072 72.1704ZM53.1935 65.107C52.4212 64.1755 51.3245 63.6046 49.9633 63.6046C46.9207 63.6046 44.7324 66.0791 44.7324 69.1314C44.7324 72.1838 46.8999 74.6582 49.9633 74.6582C51.3245 74.6582 52.4212 74.0872 53.1935 73.1558V74.386H55.882V59.7317H53.1935V65.107ZM60.9269 68.0642C61.2177 66.6446 62.157 65.9227 63.3917 65.9227C64.6158 65.9227 65.5108 66.6769 65.7964 68.0642H60.9269ZM63.4588 63.4633C60.3151 63.4633 58.3597 65.9906 58.3597 69.0819C58.3597 72.2836 60.4492 74.6582 63.5547 74.6582C65.6965 74.6582 66.9957 73.6616 67.7998 72.4067L65.8004 71.1215C65.2978 71.8124 64.4935 72.1775 63.6314 72.1775C62.2212 72.1775 61.2768 71.5131 60.9549 70.1845H66.232L68.1545 70.2011C68.2274 69.8515 68.2704 69.4902 68.2704 69.0819C68.2704 66.3893 66.6218 63.4633 63.4588 63.4633ZM76.4919 68.8771C77.2218 69.389 77.6844 70.1567 77.6844 71.1697C77.6844 73.2169 76.31 74.6374 73.7404 74.6374C71.3223 74.6374 70.0256 73.3933 69.5093 72.1368L71.7288 70.8355L71.7502 70.9107C71.8422 71.1957 72.2666 72.1933 73.7006 72.1933C74.7363 72.1933 75.0551 71.7337 75.0551 71.3278L75.0486 71.2305C75.0207 71.0345 74.8819 70.7487 74.2584 70.5012L73.9558 70.3908C72.4208 69.8698 69.9161 69.4775 69.9161 66.9708C69.9161 64.7776 71.6284 63.6078 73.7404 63.6078C75.7154 63.6078 76.984 64.8416 77.4656 65.8016L75.2941 67.0753L75.2482 66.977C75.1127 66.7133 74.6667 66.0099 73.7604 66.0099C72.645 66.0099 72.5454 66.6576 72.5454 66.8455C72.5454 67.3574 73.0304 67.5876 74.0675 67.9043L74.8331 68.1326C75.2823 68.2707 75.7403 68.4334 76.2105 68.7012L76.4919 68.8771ZM84.0722 72.0053C85.1384 72.0053 85.9583 71.4718 86.3726 70.5968L88.5999 71.9361C87.7272 73.5414 86.1011 74.6582 84.0722 74.6582C80.9949 74.6582 78.8436 72.1891 78.8436 69.1435C78.8436 66.0976 81.0147 63.6286 84.0722 63.6286C86.0713 63.6286 87.6914 64.7272 88.5735 66.3111L86.3511 67.681C85.9287 66.8174 85.1154 66.2815 84.0722 66.2815C82.5173 66.2815 81.4132 67.5628 81.4132 69.1435C81.4132 70.7239 82.5071 72.0053 84.0722 72.0053ZM93.0657 65.6551C93.5475 64.3976 94.5289 63.6783 96.0069 63.6751V66.6637C94.3406 66.4946 93.1191 67.3426 93.0682 69.3488L93.0657 69.5903V74.2597H90.5696V63.8826H93.0657V65.6551ZM97.7176 74.2597V63.8825H100.214V74.2597H97.7176ZM97.7176 62.4976V59.7317H100.296V62.4976H97.7176ZM108.012 63.5712C110.837 63.5712 112.869 66.0244 112.869 69.0504C112.869 72.0764 110.856 74.5295 108.012 74.5295C106.748 74.5295 105.73 73.9635 105.013 73.0401V78.3898H102.516V63.8618H105.013V65.0606C105.73 64.1372 106.748 63.5712 108.012 63.5712ZM107.693 66.1241C106.038 66.1241 105.013 67.4342 105.013 69.0504C105.013 70.6665 106.019 71.9768 107.693 71.9768C109.366 71.9768 110.373 70.6665 110.373 69.0504C110.373 67.4342 109.347 66.1241 107.693 66.1241ZM120.302 66.4904H118.051V70.8354C118.051 72.1933 119.366 71.9008 120.302 71.9008V74.3449L120.054 74.3763C119.827 74.4 119.45 74.4284 118.947 74.4284L118.582 74.4206C116.841 74.3429 115.482 73.6495 115.482 70.8354V66.4904H113.749V63.9001H115.482V60.9756H118.051V63.9001H120.302V66.4904Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n}\n\nfunction Duolingo() {\n  return (\n    <svg\n      aria-hidden=\"true\"\n      fill=\"none\"\n      height=\"136\"\n      preserveAspectRatio=\"xMidYMid meet\"\n      viewBox=\"0 0 136 136\"\n      width=\"136\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <g clipPath=\"url(#clip0_16318_96887)\">\n        <path\n          d=\"M105.611 67.9492C105.611 64.1701 108.49 61.5067 112.306 61.5067C116.121 61.5067 119 64.1701 119 67.9492C119 71.7283 116.121 74.3917 112.306 74.3917C108.49 74.3917 105.611 71.6923 105.611 67.9492ZM115.221 67.9492C115.221 66.1856 114.105 64.8899 112.342 64.8899C110.578 64.8899 109.462 66.1856 109.462 67.9492C109.462 69.7128 110.578 71.0085 112.342 71.0085C114.105 71.0085 115.221 69.7488 115.221 67.9492ZM103.092 64.9619C103.344 65.4658 103.452 66.0056 103.452 66.5815C103.452 67.9492 102.732 69.2089 101.58 70.1807C103.164 70.9365 104.1 72.7721 104.1 74.4996C104.1 77.7389 101.472 79.8984 97.4771 79.8984C93.482 79.8984 90.8546 77.7749 90.8546 74.4996C90.8186 72.7001 91.7904 71.0445 93.374 70.1807C92.1863 69.2089 91.5025 67.9492 91.5025 66.5815C91.5025 63.7382 93.7339 61.6507 97.2611 61.6507C99.3486 61.6507 99.9605 62.0106 101.148 62.0106C101.94 62.0466 102.768 61.8306 103.452 61.3987C103.704 61.2548 103.956 61.1468 104.243 61.1468C104.711 61.1468 104.963 61.6147 104.963 62.1905C105.071 63.4502 104.279 64.566 103.092 64.9619V64.9619ZM100.392 74.3917C100.392 72.952 99.2766 71.9442 97.5131 71.9442C95.7495 71.9442 94.6337 72.916 94.6337 74.3917C94.6337 75.7593 95.8574 76.8391 97.5131 76.8391C99.1687 76.8391 100.392 75.7593 100.392 74.3917ZM95.1736 66.7975C95.2096 68.0572 96.2893 69.0649 97.585 69.0289C98.8087 68.9929 99.7445 68.0212 99.8165 66.7975C99.8165 65.5018 98.8807 64.602 97.5131 64.602C96.1454 64.602 95.1736 65.4658 95.1736 66.7975V66.7975ZM89.127 68.2011V72.7001C89.127 73.5639 88.8031 73.9238 87.8673 73.9238H86.6076C85.6718 73.9238 85.3479 73.5639 85.3479 72.7001V68.3091C85.3479 67.1934 85.168 66.4015 84.7721 65.8977C84.3042 65.3218 83.5843 65.0339 82.8645 65.0699C82.1087 65.0339 81.3889 65.3578 80.885 65.8977C80.4531 66.4015 80.1651 67.1934 80.1651 68.2731V72.6641C80.1651 73.5639 79.7692 73.8878 78.9054 73.8878H77.6457C76.7819 73.8878 76.386 73.5639 76.386 72.6641V62.5864C76.386 62.0106 76.674 61.7586 77.1059 61.7586C77.7537 61.7586 78.6535 62.2985 79.3373 63.3423C80.3811 62.1905 81.8567 61.5427 83.4044 61.5427C85.2399 61.5427 86.7156 62.1905 87.6874 63.2703C88.6591 64.35 89.127 65.8257 89.127 68.2011ZM69.4756 58.0515C69.4756 56.8998 70.4474 56 71.5632 56H71.6351C72.7509 56.072 73.6147 57.0438 73.5427 58.1595C73.4707 59.2752 72.4989 60.139 71.3832 60.067C70.3035 60.0311 69.4756 59.1313 69.4756 58.0515V58.0515ZM69.7276 72.7001V63.2343C69.7276 62.3705 70.0515 61.9746 70.9873 61.9746H72.247C73.1828 61.9746 73.5067 62.3345 73.5067 63.2343V72.7001C73.5067 73.5639 73.1828 73.9238 72.247 73.9238H70.9873C70.0515 73.9238 69.7276 73.5639 69.7276 72.7001ZM63.0692 72.7001V60.8229C63.0692 57.8356 64.7248 56.144 66.1284 56.144C66.5603 56.144 66.8483 56.4319 66.8483 56.9718V72.7001C66.8483 73.5999 66.4884 73.9238 65.5886 73.9238H64.3289C63.4651 73.9238 63.0692 73.5999 63.0692 72.7001V72.7001ZM47.5208 67.9492C47.5208 64.1701 50.4001 61.5067 54.2152 61.5067C58.0303 61.5067 60.9097 64.1701 60.9097 67.9492C60.9097 71.7283 58.0303 74.3917 54.2152 74.3917C50.4001 74.3917 47.5208 71.6923 47.5208 67.9492ZM57.0946 67.9492C57.0946 66.1856 55.9788 64.8899 54.2152 64.8899C52.4517 64.8899 51.3359 66.1856 51.3359 67.9492C51.3359 69.7128 52.4517 71.0085 54.2152 71.0085C55.9788 71.0085 57.0946 69.7488 57.0946 67.9492ZM45.5773 63.2343V73.3119C45.5773 73.8878 45.2893 74.1397 44.8574 74.1397C44.2096 74.1397 43.3098 73.5999 42.662 72.5921C41.6542 73.7438 40.2145 74.3917 38.6669 74.3557C37.2272 74.3917 35.8596 73.8878 34.7798 72.916C33.6641 71.8363 33.0522 70.1807 33.0522 67.8052V63.2343C33.0522 62.3705 33.3761 61.9746 34.3119 61.9746H35.5716C36.5074 61.9746 36.8313 62.3345 36.8313 63.2343V67.4813C36.8313 68.921 37.0833 69.6768 37.5512 70.1807C37.9831 70.6126 38.6309 70.8645 39.2428 70.8285C39.9266 70.8285 40.5745 70.5766 41.0423 70.0727C41.5102 69.5688 41.7622 68.777 41.7622 67.5533V63.2343C41.7622 62.3345 42.1581 61.9746 43.0219 61.9746H44.2816C45.1814 61.9746 45.5773 62.3345 45.5773 63.2343V63.2343ZM30.4608 56.9718V73.3119C30.4608 73.8878 30.1729 74.1397 29.741 74.1397C29.0932 74.1397 28.1934 73.5999 27.5455 72.6281C26.7537 73.5639 25.1341 74.3917 23.2625 74.3917C19.5194 74.3917 17 71.6563 17 67.9492C17 64.2421 19.5914 61.5067 23.2625 61.5067C24.4862 61.5067 25.674 61.8666 26.7177 62.5505V60.8229C26.7177 57.8356 28.4093 56.144 29.777 56.144C30.2089 56.144 30.4608 56.4319 30.4608 56.9718V56.9718ZM26.7177 67.9492C26.7177 66.1136 25.422 64.8899 23.8024 64.8899C22.1828 64.8899 20.8511 66.1136 20.8511 67.9492C20.8511 69.7848 22.1468 71.0085 23.8024 71.0085C25.458 71.0085 26.7177 69.8207 26.7177 67.9492V67.9492Z\"\n          fill=\"currentColor\"\n        />\n      </g>\n      <defs>\n        <clipPath id=\"clip0_16318_96887\">\n          <rect\n            fill=\"white\"\n            height=\"23.8984\"\n            transform=\"translate(17 56)\"\n            width=\"102\"\n          />\n        </clipPath>\n      </defs>\n    </svg>\n  );\n}\n\nfunction Faire() {\n  return (\n    <svg\n      aria-hidden=\"true\"\n      fill=\"none\"\n      height=\"136\"\n      preserveAspectRatio=\"xMidYMid meet\"\n      viewBox=\"0 0 136 136\"\n      width=\"136\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <g clipPath=\"url(#clip0_16329_10567)\">\n        <path\n          clipRule=\"evenodd\"\n          d=\"M27.254 65.2234L27.0762 65.312C26.5902 64.7098 26.128 64.1962 25.6894 63.7711C25.2509 63.346 24.8242 63.0332 24.4094 62.8324C24.2079 62.738 23.8434 62.6583 23.316 62.5933C22.7885 62.5284 22.1752 62.4959 21.4759 62.4959C21.3573 62.4959 21.221 62.4989 21.067 62.5048C20.9129 62.5107 20.7647 62.5166 20.6225 62.5225C20.4803 62.5284 20.3469 62.5343 20.2225 62.5402C20.098 62.5461 20.0062 62.549 19.9469 62.549C19.935 62.6199 19.9202 62.6907 19.9024 62.7616C19.8847 62.8324 19.8699 62.9151 19.858 63.0095C19.8461 63.104 19.8343 63.225 19.8224 63.3726C19.8106 63.5202 19.7987 63.7003 19.7869 63.9128C19.775 64.3261 19.7661 64.7777 19.7602 65.2677C19.7543 65.7577 19.7513 66.2212 19.7513 66.658V68.3406H20.0713C20.261 68.3406 20.4714 68.3347 20.7025 68.3229C20.9336 68.3111 21.1736 68.2963 21.4225 68.2786C21.6714 68.2609 21.8789 68.2343 22.0448 68.1989C22.4715 68.1045 22.8093 67.9805 23.0582 67.827C23.3071 67.6735 23.5026 67.4905 23.6449 67.2779C23.7871 67.0654 23.8879 66.8322 23.9471 66.5783C24.0064 66.3245 24.0597 66.05 24.1071 65.7548H24.3205V71.4578H24.1071C24.0123 71.0799 23.876 70.6814 23.6982 70.2623C23.5204 69.8431 23.3071 69.5213 23.0582 69.297C22.833 69.0963 22.593 68.9457 22.3381 68.8454C22.0833 68.745 21.784 68.683 21.4403 68.6594C21.2625 68.6476 21.1084 68.6387 20.9781 68.6328C20.8477 68.6269 20.7232 68.624 20.6047 68.624H19.7513V70.8202C19.7513 71.1508 19.7543 71.4548 19.7602 71.7323C19.7661 72.0098 19.7691 72.2489 19.7691 72.4496C19.781 72.8274 19.8017 73.1846 19.8313 73.5211C19.861 73.8576 19.9291 74.1085 20.0358 74.2738C20.1425 74.4391 20.3232 74.5661 20.578 74.6546C20.8329 74.7432 21.2033 74.7993 21.6892 74.8229V75H16V74.8229C16.2963 74.7757 16.5719 74.6989 16.8267 74.5926C17.0815 74.4864 17.2919 74.3388 17.4579 74.1499C17.5171 74.079 17.5645 73.9757 17.6001 73.8399C17.6356 73.7041 17.6682 73.5477 17.6979 73.3706C17.7275 73.1935 17.7482 73.0075 17.7601 72.8127C17.772 72.6178 17.7779 72.426 17.7779 72.2371C17.7779 72.0718 17.7808 71.8474 17.7868 71.564C17.7927 71.2807 17.7986 70.9707 17.8045 70.6342C17.8105 70.2977 17.8134 69.9523 17.8134 69.5981V65.1172C17.8134 64.9046 17.8105 64.6862 17.8045 64.4619C17.7986 64.2375 17.7868 64.025 17.769 63.8243C17.7512 63.6235 17.7216 63.4435 17.6801 63.2841C17.6386 63.1247 17.5823 63.0095 17.5112 62.9387C17.3334 62.7498 17.1438 62.6287 16.9423 62.5756C16.7408 62.5225 16.48 62.4723 16.16 62.4251V62.248H26.045L27.254 65.2234ZM46.6803 75V74.8229C46.7158 74.8229 46.8018 74.8111 46.9381 74.7875C47.0744 74.7639 47.2166 74.7225 47.3647 74.6635C47.5129 74.6044 47.6433 74.5218 47.7559 74.4155C47.8685 74.3093 47.9248 74.1735 47.9248 74.0082C47.9248 73.9019 47.8922 73.7277 47.827 73.4857C47.7618 73.2436 47.67 72.9485 47.5514 72.6001C47.4329 72.2518 47.2966 71.8651 47.1425 71.4401C46.9884 71.015 46.8225 70.5722 46.6447 70.1117H41.9156C41.7378 70.5604 41.5659 70.9884 41.4 71.3958C41.234 71.8031 41.0681 72.2134 40.9022 72.6267C40.6533 73.2643 40.5288 73.713 40.5288 73.9728C40.5288 74.1262 40.5762 74.2532 40.671 74.3535C40.7659 74.4539 40.8873 74.5366 41.0355 74.6015C41.1837 74.6664 41.3437 74.7137 41.5155 74.7432C41.6874 74.7727 41.8504 74.7993 42.0044 74.8229V75H38.2176V74.8052C38.5257 74.7698 38.7894 74.6871 39.0087 74.5572C39.228 74.4273 39.3554 74.3388 39.391 74.2916C39.4739 74.2089 39.6102 74.0141 39.7999 73.7071C39.9895 73.4001 40.2206 72.9455 40.4932 72.3433C40.7422 71.7884 41.0207 71.1567 41.3288 70.4482C41.637 69.7398 41.9541 69.0107 42.28 68.2609C42.606 67.5111 42.9289 66.7555 43.249 65.9939C43.569 65.2323 43.8712 64.5209 44.1557 63.8597L43.9779 63.47C44.1912 63.3756 44.3838 63.2634 44.5557 63.1335C44.7276 63.0036 44.8787 62.8678 45.0091 62.7262C45.1394 62.5845 45.2491 62.4487 45.338 62.3188C45.4269 62.1889 45.495 62.0827 45.5424 62H45.6847C46.0521 62.98 46.4017 63.9099 46.7336 64.7895C47.0655 65.6692 47.3825 66.5163 47.6848 67.3311C47.987 68.1458 48.2804 68.9339 48.5648 69.6955C48.8493 70.4571 49.1337 71.2098 49.4182 71.9537C49.5486 72.3197 49.6701 72.6237 49.7827 72.8658C49.8953 73.1079 50.0227 73.3824 50.1649 73.6894C50.1886 73.7366 50.242 73.8193 50.3249 73.9373C50.4079 74.0554 50.5175 74.1764 50.6538 74.3004C50.7901 74.4244 50.959 74.5336 51.1605 74.6281C51.362 74.7225 51.5991 74.7757 51.8717 74.7875V75H46.6803ZM44.3512 64.267C44.2801 64.4205 44.1764 64.6626 44.0401 64.9932C43.9038 65.3238 43.7349 65.7252 43.5334 66.1975C43.3319 66.6698 43.1097 67.2041 42.8667 67.8004C42.6237 68.3967 42.36 69.0431 42.0756 69.7398H46.5025C46.301 69.2084 46.0965 68.6771 45.8891 68.1458C45.6817 67.6144 45.4802 67.1038 45.2846 66.6138C45.0891 66.1237 44.9113 65.6751 44.7513 65.2677C44.5913 64.8604 44.4579 64.5268 44.3512 64.267ZM69.3601 75H63.9731V74.8229C64.2694 74.7757 64.545 74.6989 64.7998 74.5926C65.0546 74.4864 65.271 74.3329 65.4487 74.1322C65.508 74.0613 65.5525 73.9609 65.5821 73.8311C65.6117 73.7012 65.6354 73.5477 65.6532 73.3706C65.671 73.1935 65.6828 73.0075 65.6888 72.8127C65.6947 72.6178 65.6976 72.426 65.6976 72.2371C65.6976 72.0718 65.7006 71.8474 65.7065 71.564C65.7125 71.2807 65.7184 70.9707 65.7243 70.6342C65.7302 70.2977 65.7332 69.9523 65.7332 69.5981V64.4796C65.7332 64.267 65.7243 64.0693 65.7065 63.8862C65.6888 63.7032 65.6621 63.5379 65.6265 63.3903C65.591 63.2427 65.5376 63.1276 65.4665 63.045C65.3006 62.8442 65.0961 62.6996 64.8532 62.611C64.6102 62.5225 64.3346 62.4605 64.0264 62.4251V62.248H69.3423V62.4251C69.0934 62.4605 68.8267 62.5254 68.5423 62.6199C68.2578 62.7144 68.0385 62.9033 67.8844 63.1866C67.7778 63.3874 67.7215 63.6412 67.7155 63.9482C67.7096 64.2552 67.7007 64.5445 67.6889 64.8161C67.6889 65.1113 67.6859 65.4035 67.68 65.6928C67.6741 65.9821 67.6711 66.2743 67.6711 66.5695V72.5027C67.6711 72.8451 67.677 73.1757 67.6889 73.4946C67.7007 73.8134 67.7541 74.0377 67.8489 74.1676C67.9911 74.3683 68.2163 74.5218 68.5245 74.6281C68.8326 74.7343 69.1112 74.7993 69.3601 74.8229V75ZM96.2712 75H92.6266C92.5199 74.8937 92.4429 74.814 92.3955 74.7609C92.3481 74.7078 92.2947 74.6458 92.2355 74.5749C92.1762 74.5041 92.1021 74.4008 92.0132 74.265C91.9243 74.1292 91.7851 73.9255 91.5954 73.654C91.3702 73.3233 91.1717 73.0252 90.9998 72.7595C90.828 72.4939 90.6531 72.2312 90.4754 71.9714C90.2976 71.7116 90.105 71.4489 89.8975 71.1832C89.6901 70.9176 89.4442 70.6194 89.1597 70.2888C88.9227 70.0173 88.6649 69.7634 88.3863 69.5272C88.1078 69.2911 87.8145 69.1435 87.5063 69.0845C87.2811 69.0372 87.1092 69.0136 86.9907 69.0136C86.884 69.0136 86.7477 69.0077 86.5818 68.9959V70.9973C86.5818 71.564 86.5877 72.0422 86.5996 72.4319C86.6114 72.8097 86.6233 73.1344 86.6351 73.406C86.647 73.6776 86.7122 73.9019 86.8307 74.079C86.9255 74.2325 87.1092 74.389 87.3818 74.5484C87.6545 74.7078 88.0337 74.7993 88.5197 74.8229V75H82.8305V74.8229C83.1268 74.7757 83.4112 74.7048 83.6839 74.6104C83.9565 74.5159 84.1698 74.3742 84.3239 74.1853C84.3832 74.1144 84.4306 74.0082 84.4661 73.8665C84.5017 73.7248 84.5283 73.5654 84.5461 73.3883C84.5639 73.2112 84.5787 73.0223 84.5906 72.8215C84.6024 72.6208 84.6084 72.426 84.6084 72.2371C84.6084 72.0718 84.6113 71.8474 84.6172 71.564C84.6232 71.2807 84.6291 70.9707 84.635 70.6342C84.641 70.2977 84.6439 69.9523 84.6439 69.5981V65.1172C84.6439 64.9046 84.641 64.6891 84.635 64.4707C84.6291 64.2523 84.6172 64.0456 84.5995 63.8508C84.5817 63.656 84.5521 63.4789 84.5106 63.3195C84.4691 63.1601 84.4128 63.045 84.3417 62.9741C84.1639 62.7852 83.9654 62.6583 83.7461 62.5933C83.5268 62.5284 83.2572 62.4723 82.9371 62.4251V62.248H83.6661C84.0691 62.248 84.5254 62.245 85.035 62.2391C85.5447 62.2332 86.0544 62.2302 86.564 62.2302H87.8085C88.6027 62.2302 89.3079 62.2539 89.9242 62.3011C90.5405 62.3483 91.1035 62.5077 91.6132 62.7793C92.1347 63.0509 92.5555 63.4198 92.8755 63.8862C93.1955 64.3526 93.3555 64.9401 93.3555 65.6485C93.3555 66.0854 93.2755 66.478 93.1155 66.8263C92.9555 67.1746 92.7422 67.4816 92.4755 67.7473C92.2088 68.0129 91.8977 68.2402 91.5421 68.4292C91.1865 68.6181 90.8132 68.7775 90.422 68.9074V68.9605C90.6235 69.0668 90.8191 69.2203 91.0087 69.421C91.1984 69.6217 91.3821 69.846 91.5599 70.094C91.868 70.5427 92.2029 71.0268 92.5644 71.5463C92.9259 72.0659 93.2488 72.5263 93.5333 72.9278C93.9719 73.5418 94.3985 73.9905 94.8134 74.2738C95.2282 74.5572 95.7142 74.7402 96.2712 74.8229V75ZM91.311 65.5422C91.311 64.9519 91.1628 64.4264 90.8665 63.9659C90.5702 63.5054 90.1376 63.1394 89.5686 62.8678C89.2842 62.738 88.9849 62.6435 88.6708 62.5845C88.3567 62.5254 88.0278 62.4959 87.6841 62.4959C87.4352 62.4959 87.2337 62.5018 87.0796 62.5136C86.9255 62.5254 86.807 62.5372 86.724 62.549C86.7122 62.6199 86.7003 62.6819 86.6885 62.735C86.6766 62.7881 86.6677 62.856 86.6618 62.9387C86.6559 63.0213 86.65 63.1365 86.644 63.2841C86.6381 63.4317 86.6292 63.6412 86.6174 63.9128C86.6055 64.3261 86.5966 64.7925 86.5907 65.312C86.5848 65.8315 86.5818 66.3333 86.5818 66.8174V68.7657C87.0678 68.7657 87.5211 68.745 87.9419 68.7037C88.3626 68.6624 88.7686 68.5767 89.1597 68.4469C89.539 68.3288 89.8649 68.1694 90.1376 67.9687C90.4102 67.7679 90.6324 67.5406 90.8043 67.2868C90.9761 67.0329 91.1035 66.7584 91.1865 66.4632C91.2695 66.168 91.311 65.861 91.311 65.5422ZM120 71.6172L119.111 75H108.23V74.8229C108.527 74.7757 108.802 74.6989 109.057 74.5926C109.312 74.4864 109.522 74.3388 109.688 74.1499C109.748 74.079 109.795 73.9757 109.831 73.8399C109.866 73.7041 109.899 73.5477 109.928 73.3706C109.958 73.1935 109.979 73.0075 109.991 72.8127C110.002 72.6178 110.008 72.426 110.008 72.2371C110.008 72.0718 110.011 71.8474 110.017 71.564C110.023 71.2807 110.029 70.9707 110.035 70.6342C110.041 70.2977 110.044 69.9523 110.044 69.5981C110.044 69.2321 110.044 68.8896 110.044 68.5708V65.1172C110.044 64.9046 110.041 64.6862 110.035 64.4619C110.029 64.2375 110.017 64.025 109.999 63.8243C109.982 63.6235 109.952 63.4435 109.911 63.2841C109.869 63.1247 109.813 63.0095 109.742 62.9387C109.564 62.7498 109.374 62.6287 109.173 62.5756C108.971 62.5225 108.71 62.4723 108.39 62.4251V62.248H118.204L119.413 65.1703L119.236 65.2589C118.275 64.0663 117.375 63.2634 116.533 62.8501C116.332 62.7557 115.958 62.673 115.413 62.6022C114.868 62.5313 114.204 62.4959 113.422 62.4959C113.173 62.4959 112.915 62.5048 112.648 62.5225C112.382 62.5402 112.189 62.5609 112.071 62.5845C112.047 62.7734 112.026 63.163 112.008 63.7534C111.991 64.3438 111.982 65.0286 111.982 65.8079C111.982 66.2566 111.982 66.6344 111.982 66.9414C111.982 67.2484 111.985 67.4993 111.991 67.6941C111.997 67.889 112 68.0395 112 68.1458C112 68.2402 112 68.3052 112 68.3406C112.059 68.3406 112.177 68.3406 112.355 68.3406C112.533 68.3406 112.737 68.3347 112.968 68.3229C113.2 68.3111 113.431 68.2963 113.662 68.2786C113.893 68.2609 114.086 68.2343 114.24 68.1989C114.595 68.1163 114.892 68.0041 115.129 67.8624C115.366 67.7207 115.558 67.5495 115.706 67.3488C115.855 67.148 115.973 66.9149 116.062 66.6492C116.151 66.3835 116.231 66.0854 116.302 65.7548H116.515V71.3338H116.302C116.207 70.9559 116.062 70.5663 115.866 70.1649C115.671 69.7634 115.466 69.4623 115.253 69.2616C115.063 69.0963 114.835 68.9605 114.569 68.8542C114.302 68.748 113.991 68.683 113.635 68.6594C113.457 68.6476 113.309 68.6387 113.191 68.6328C113.072 68.6269 112.954 68.624 112.835 68.624C112.728 68.624 112.61 68.624 112.48 68.624C112.361 68.624 112.207 68.624 112.017 68.624C111.994 68.7893 111.982 69.0313 111.982 69.3501C111.982 69.6689 111.982 69.9995 111.982 70.342C111.982 70.4955 111.982 70.6726 111.982 70.8733C111.982 71.074 111.982 71.2747 111.982 71.4755C111.982 71.6644 111.985 71.8445 111.991 72.0157C111.997 72.1869 112 72.3315 112 72.4496C112.011 72.8274 112.032 73.1846 112.062 73.5211C112.091 73.8576 112.16 74.1085 112.266 74.2738C112.373 74.4391 112.628 74.5572 113.031 74.6281C113.434 74.6989 113.872 74.7343 114.346 74.7343C114.548 74.7343 114.806 74.7284 115.12 74.7166C115.434 74.7048 115.763 74.6753 116.106 74.6281C116.486 74.5808 116.835 74.4893 117.155 74.3535C117.475 74.2178 117.784 74.0259 118.08 73.7779C118.376 73.53 118.664 73.2259 118.942 72.8658C119.221 72.5057 119.508 72.0718 119.804 71.564L120 71.6172Z\"\n          fill=\"currentColor\"\n          fillRule=\"evenodd\"\n        />\n      </g>\n      <defs>\n        <clipPath id=\"clip0_16329_10567\">\n          <rect\n            fill=\"white\"\n            height=\"13\"\n            transform=\"translate(16 62)\"\n            width=\"104\"\n          />\n        </clipPath>\n      </defs>\n    </svg>\n  );\n}\n\nfunction _Gainsight() {\n  return (\n    <svg\n      aria-hidden=\"true\"\n      fill=\"none\"\n      height=\"136\"\n      preserveAspectRatio=\"xMidYMid meet\"\n      viewBox=\"0 0 136 136\"\n      width=\"136\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <g clipPath=\"url(#clip0_16329_10576)\">\n        <mask\n          height=\"25\"\n          id=\"mask0_16329_10576\"\n          maskUnits=\"userSpaceOnUse\"\n          style={{ maskType: \"luminance\" }}\n          width=\"112\"\n          x=\"12\"\n          y=\"56\"\n        >\n          <path d=\"M124 56H12V80.2945H124V56Z\" fill=\"white\" />\n        </mask>\n        <g mask=\"url(#mask0_16329_10576)\">\n          <path\n            d=\"M21.7226 68.1472H26.5958V71.7217C25.2552 72.6792 23.6441 73.1842 21.9967 73.1634C18.1005 73.1574 15.515 70.2978 15.515 66.348V66.2944C15.515 62.6485 18.1839 59.5744 21.6929 59.5744C22.6773 59.5432 23.6582 59.7077 24.5786 60.0585C25.499 60.4093 26.3406 60.9393 27.0546 61.6178L29.1575 59.08C27.1975 57.4178 25.1541 56.4885 21.8299 56.4885C20.5323 56.4732 19.2447 56.718 18.0433 57.2085C16.8418 57.699 15.7508 58.4251 14.8346 59.3441C13.9183 60.2631 13.1955 61.3564 12.7087 62.5593C12.2218 63.7622 11.9809 65.0505 12.0001 66.348V66.4017C12.0001 71.96 15.9201 76.2076 21.8835 76.2076C24.8067 76.2045 27.6307 75.1469 29.8367 73.2289V65.234H21.7226V68.1472Z\"\n            fill=\"currentColor\"\n          />\n          <path\n            d=\"M37.4142 61.308C35.522 61.2794 33.6486 61.6871 31.9393 62.4995L32.8508 65.1506C34.142 64.5634 35.5431 64.2567 36.9615 64.251C39.2491 64.251 40.5002 65.3412 40.5002 67.331V67.5991C39.2179 67.1812 37.875 66.9799 36.5266 67.0034C33.1785 67.0034 30.7002 68.5285 30.7002 71.7157V71.7693C30.7002 74.6587 33.0832 76.2076 35.8176 76.2076C36.7001 76.2386 37.578 76.0686 38.385 75.7104C39.1921 75.3523 39.9072 74.8154 40.4764 74.1404V75.9276H43.747V67.3846C43.747 63.5421 41.6738 61.308 37.4262 61.308M40.5598 70.7089C40.5598 72.4961 38.9215 73.7293 36.747 73.7293C35.1921 73.7293 33.9649 72.9668 33.9649 71.6025V71.5489C33.9649 70.1608 35.1921 69.3149 37.2593 69.3149C38.3863 69.3144 39.5041 69.5162 40.5598 69.9106V70.7089Z\"\n            fill=\"currentColor\"\n          />\n          <path\n            d=\"M48.8892 61.5464H45.5947V75.9277H48.8892V61.5464Z\"\n            fill=\"currentColor\"\n          />\n          <path\n            d=\"M48.9958 56.2085H45.457V59.3421H48.9958V56.2085Z\"\n            fill=\"currentColor\"\n          />\n          <path\n            d=\"M54.1675 67.8136C54.1675 65.5795 55.502 64.2391 57.4084 64.2391C59.3147 64.2391 60.4884 65.5199 60.4884 67.748V75.9276H63.7828V66.777C63.7828 63.4289 61.9062 61.2485 58.6594 61.2485C57.7547 61.2412 56.864 61.4711 56.0758 61.9154C55.2877 62.3596 54.6297 63.0026 54.1675 63.7804V61.5463H50.873V75.9276H54.1675V67.8136Z\"\n            fill=\"currentColor\"\n          />\n          <path\n            d=\"M70.4485 73.5924C68.727 73.5156 67.0755 72.8893 65.7361 71.8051L64.2646 74.0392C66.0109 75.4137 68.1608 76.1757 70.3829 76.2077C73.3617 76.2077 75.7446 74.7124 75.7446 71.686V71.6324C75.7446 69.0468 73.3617 68.0936 71.2825 67.4621C69.5489 66.8962 68 66.4553 68 65.4247V65.3711C68 64.5251 68.7327 63.9234 70.0136 63.9234C71.4616 64.0175 72.86 64.4878 74.0706 65.2877L75.3753 62.9464C73.8053 61.914 71.9756 61.346 70.097 61.3081C67.2374 61.3081 64.9736 62.9702 64.9736 65.6987V65.7524C64.9736 68.4987 67.3566 69.3745 69.4655 69.9702C71.1574 70.4885 72.6825 70.8698 72.6825 71.9839V72.0434C72.6825 72.9966 71.8664 73.5924 70.4485 73.5924Z\"\n            fill=\"currentColor\"\n          />\n          <path\n            d=\"M80.0465 56.1609H76.5078V59.2945H80.0465V56.1609Z\"\n            fill=\"currentColor\"\n          />\n          <path\n            d=\"M76.6445 61.5464V75.9277H77.9552H79.939V75.1234V61.5464H76.6445Z\"\n            fill=\"currentColor\"\n          />\n          <path\n            d=\"M92.6223 63.6554C92.0332 62.88 91.2702 62.2538 90.3948 61.8275C89.5193 61.4011 88.5559 61.1865 87.5823 61.2009C85.9971 61.1773 84.4592 61.7402 83.2639 62.7816C82.0686 63.8229 81.3003 65.2692 81.1065 66.8426C81.0538 67.2117 81.026 67.5839 81.0231 67.9567V68.0103C80.9883 69.0631 81.2079 70.1088 81.6632 71.0587C82.1184 72.0087 82.796 72.8349 83.6384 73.4673C83.8622 73.6255 84.095 73.7707 84.3354 73.9022L84.5082 73.9916L89.6316 71.7933C89.4359 71.8476 89.2369 71.8894 89.0359 71.9184C88.8382 71.9432 88.6393 71.9571 88.4401 71.9601H88.1482C87.9582 71.9484 87.7692 71.9245 87.5823 71.8886C86.7991 71.7498 86.0752 71.3806 85.5031 70.8281C85.2712 70.593 85.0709 70.3287 84.9074 70.0418C84.8537 69.9464 84.8061 69.8511 84.7584 69.7558C84.6164 69.4525 84.5142 69.1321 84.4546 68.8026C84.4518 68.7669 84.4518 68.7311 84.4546 68.6954C84.4185 68.4648 84.4006 68.2317 84.401 67.9984V67.915C84.399 67.6311 84.425 67.3478 84.4784 67.069C84.4784 66.9856 84.5201 66.9081 84.538 66.8307C84.585 66.65 84.6447 66.4728 84.7167 66.3005C84.8172 66.0644 84.941 65.8389 85.0861 65.6273C85.3789 65.2102 85.754 64.8574 86.1882 64.5907C86.885 64.1834 87.6809 63.9772 88.4878 63.995C89.2653 64.0005 90.0284 64.2056 90.704 64.5907L90.9542 64.7396C91.1295 64.8552 91.2967 64.9826 91.4546 65.1209C91.7434 65.3838 91.9905 65.6892 92.1874 66.0264C92.2528 66.1405 92.3125 66.2578 92.3661 66.3779C92.44 66.542 92.5017 66.7112 92.5508 66.8843C92.6518 67.2471 92.7038 67.6218 92.7057 67.9984V68.052C92.7035 68.3921 92.6575 68.7305 92.5686 69.0588C92.4713 69.4319 92.3165 69.7876 92.1099 70.1133C92.0503 70.2026 91.9908 70.292 91.9252 70.3754C91.7929 70.5434 91.6474 70.7008 91.4903 70.846L91.2461 71.0545C91.1795 71.1137 91.1099 71.1694 91.0376 71.2213L90.8469 71.3405L95.9167 69.1064V61.4988H92.6223V63.6554Z\"\n            fill=\"currentColor\"\n          />\n          <path\n            d=\"M101.07 66.7889C101.101 66.6316 101.143 66.4763 101.195 66.3243C101.39 65.6921 101.788 65.1416 102.327 64.7574C102.589 64.5715 102.881 64.4305 103.19 64.3404C103.521 64.243 103.864 64.1948 104.209 64.1974C104.346 64.1882 104.483 64.1882 104.62 64.1974C104.868 64.2215 105.112 64.2756 105.347 64.3583C105.459 64.3967 105.569 64.4425 105.675 64.4953C105.789 64.5504 105.898 64.6142 106.002 64.686C106.019 64.705 106.04 64.7211 106.062 64.7336L109.636 63.2026V63.1728C109.589 63.1013 109.547 63.0298 109.493 62.9643C109.015 62.3714 108.402 61.9004 107.706 61.5897C107.011 61.2791 106.251 61.1377 105.49 61.177C104.577 61.1688 103.678 61.4019 102.884 61.8528C102.09 62.3037 101.429 62.9563 100.968 63.7447V56H97.6738V68.3557L101.052 66.9081\"\n            fill=\"currentColor\"\n          />\n          <path\n            d=\"M115.416 57.5488H112.121V61.8501V62.0586L115.416 60.6705V57.5488Z\"\n            fill=\"currentColor\"\n          />\n          <path\n            d=\"M90.2929 71.5728C90.0859 71.6613 89.8729 71.7349 89.6554 71.7933L84.532 73.9916C85.4747 74.4797 86.5206 74.7351 87.5822 74.7362C88.5806 74.7477 89.5661 74.5116 90.4509 74.0489C91.3356 73.5863 92.0919 72.9116 92.652 72.0852V73.1992C92.6927 74.1671 92.4393 75.1245 91.9252 75.9456C91.1865 77.0418 89.8937 77.6137 88.1005 77.6137C86.2826 77.6144 84.504 77.0844 82.9831 76.0886L81.7559 78.5669C83.7026 79.7129 85.9251 80.3062 88.1839 80.2826C90.7695 80.2826 92.7831 79.6571 94.0937 78.3464C94.7471 77.6767 95.2294 76.8593 95.4997 75.9635C95.7919 74.9838 95.9325 73.9652 95.9167 72.943V69.1064L90.8171 71.2988C90.6444 71.4001 90.4716 71.4894 90.2929 71.5728Z\"\n            fill=\"#727272\"\n          />\n          <path\n            d=\"M106.062 64.7575C106.385 65.0101 106.653 65.3248 106.852 65.6829C107.05 66.0411 107.175 66.4354 107.218 66.8426C107.264 67.1303 107.288 67.421 107.29 67.7124V75.88H110.584V66.7234C110.623 65.4861 110.289 64.2656 109.625 63.2205L106.062 64.7575Z\"\n            fill=\"#727272\"\n          />\n          <path\n            d=\"M97.6738 75.8801H100.968V67.7661C100.967 67.4781 100.995 67.1907 101.052 66.9082L97.6738 68.3559V75.8801Z\"\n            fill=\"#727272\"\n          />\n          <path\n            d=\"M124 57.406V57.0784L117.018 59.9975L112.121 62.0588V71.9065C112.121 74.3728 113.17 75.5584 114.784 75.9575C115.268 76.0732 115.764 76.1292 116.262 76.1243C116.773 76.1282 117.283 76.0722 117.781 75.9575C118.29 75.832 118.778 75.6312 119.228 75.3618V72.6809C118.599 73.0076 117.9 73.1773 117.191 73.1754C116.071 73.1754 115.404 72.6571 115.404 71.3882V64.2392L123.965 60.6648L124 57.406Z\"\n            fill=\"#727272\"\n          />\n        </g>\n      </g>\n      <defs>\n        <clipPath id=\"clip0_16329_10576\">\n          <rect\n            fill=\"white\"\n            height=\"24.2945\"\n            transform=\"translate(12 56)\"\n            width=\"112\"\n          />\n        </clipPath>\n      </defs>\n    </svg>\n  );\n}\n\nfunction _Handshake() {\n  return (\n    <svg\n      aria-hidden=\"true\"\n      fill=\"none\"\n      height=\"136\"\n      preserveAspectRatio=\"xMidYMid meet\"\n      viewBox=\"0 0 136 136\"\n      width=\"136\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <g clipPath=\"url(#clip0_16329_10574)\">\n        <path\n          d=\"M27.7222 58L26.3815 65.6109L22.4426 68.8235L24.3476 58H20.5307L17 77.9634H20.818L21.7985 72.4408L25.7499 69.2032L24.1914 77.9634H28.0095L31.5402 58H27.7222Z\"\n          fill=\"currentColor\"\n        />\n        <path\n          d=\"M36.7643 69.2503L35.1101 70.6764C33.8789 71.7313 33.2313 72.701 33.09 73.5002C32.9543 74.271 33.2108 74.8405 33.8971 74.8405C34.7613 74.8405 35.4898 74.0993 36.0336 73.3854L36.7643 69.2503ZM35.2806 77.9781L35.9914 75.9818H35.6334C34.9847 76.781 33.7683 78.2634 32.0092 78.2634C30.0403 78.2634 29.199 76.781 29.5569 74.7552C29.94 72.5874 31.1724 71.0197 33.603 69.0792L37.3184 66.1133L37.3891 65.7143C37.6216 64.4024 37.3537 63.718 36.5489 63.718C35.744 63.718 35.2355 64.4024 35.003 65.7143L34.6746 67.5684H30.9466L31.1336 66.5134C31.7185 63.2053 33.9313 61.1511 37.0026 61.1511C40.0739 61.1511 41.5606 63.2042 40.9711 66.5418L38.9486 77.9781H35.2806Z\"\n          fill=\"currentColor\"\n        />\n        <path\n          d=\"M40.208 77.9775L43.133 61.4365H46.8609L46.1597 63.3758H46.5177C47.6999 61.9219 48.9688 61.1511 50.3117 61.1511C51.803 61.1511 53.1767 62.3209 52.6124 65.5142L50.4086 77.9769H46.6807L48.6975 66.5691C48.8947 65.4573 48.7226 64.7434 47.887 64.7434C47.1116 64.7434 46.4733 65.4846 46.0447 66.0553L43.9365 77.9769L40.208 77.9775Z\"\n          fill=\"currentColor\"\n        />\n        <path\n          d=\"M60.3689 65.6862C60.1374 64.9734 59.69 64.4596 59.0048 64.4596C58.1394 64.4596 57.6209 65.3724 57.4042 66.599L56.3495 72.5593C56.1331 73.7859 56.3291 74.6988 57.1945 74.6988C57.8808 74.6988 58.5076 74.1861 58.9924 73.4722L60.3689 65.6862ZM61.9222 77.9779L58.1943 77.9784L58.8008 76.0675H58.4428C57.3859 77.3225 56.4146 78.2638 54.9532 78.2638C52.8952 78.2638 51.7918 76.0675 52.8154 70.2789L53.0172 69.1375C54.0411 63.3478 55.8012 61.1526 58.0676 61.1526C59.291 61.1526 60.0389 61.9802 60.4995 62.9215H60.8573L61.7249 58.0139H65.453L61.9222 77.9779Z\"\n          fill=\"currentColor\"\n        />\n        <path\n          d=\"M74.3699 67.0282L70.6418 67.0277L70.9188 65.4589C71.1059 64.4039 70.9337 63.6912 70.0683 63.6912C69.4116 63.6912 68.9637 64.205 68.8725 64.7177C68.7162 65.6021 69.1473 66.2001 69.8449 67.1425L71.8913 69.9083C72.7532 71.1065 73.5455 72.3615 73.2126 74.243C72.7942 76.6098 70.5324 78.2638 67.7599 78.2638C64.9871 78.2638 63.0456 76.7519 63.6009 73.6154L63.8529 72.1899H67.5807L67.2627 73.9872C67.0814 75.0137 67.4269 75.7549 68.2329 75.7549C68.9786 75.7549 69.3776 75.1842 69.4835 74.5851C69.6295 73.7575 69.1689 73.1596 68.4951 72.2467L66.4796 69.4809C65.6427 68.3111 64.929 67.1141 65.2676 65.2031C65.6154 63.2353 67.6834 61.1538 70.5769 61.1538C73.4702 61.1538 75.071 63.0648 74.5818 65.8306L74.3699 67.0282Z\"\n          fill=\"currentColor\"\n        />\n        <path\n          d=\"M103.406 77.9784L101.109 70.5631H100.751L99.44 77.9784H95.7119L99.2415 58.0139H102.971L101.211 67.9678H101.568L105.885 61.4368H109.882L104.786 69.1648L107.612 77.9784H103.406Z\"\n          fill=\"currentColor\"\n        />\n        <path\n          d=\"M112.046 68.4233H114.85L115.324 65.7427C115.53 64.5729 115.264 63.718 114.281 63.718C113.297 63.718 112.728 64.574 112.52 65.7427L112.046 68.4233ZM111.618 70.848L111.118 73.6719C110.912 74.8416 111.178 75.6965 112.162 75.6965C113.147 75.6965 113.715 74.8405 113.923 73.6719L114.18 72.2179H118.177C117.578 75.6113 114.931 78.2634 111.71 78.2634C108.489 78.2634 106.779 75.9534 107.788 70.2489L107.98 69.1656C108.989 63.4611 111.515 61.1511 114.735 61.1511C117.956 61.1511 119.726 63.4611 118.717 69.1656L118.419 70.848H111.618Z\"\n          fill=\"currentColor\"\n        />\n        <path\n          d=\"M73.3848 77.9773L76.9154 58.0139H80.6435L79.7508 63.0613H80.1088C81.2989 61.8915 82.3855 61.1503 83.7272 61.1503C85.2184 61.1503 86.3539 62.3201 85.7884 65.5134L83.5848 77.9762H79.8567L81.9191 66.3114C82.1164 65.1996 81.9442 64.4857 81.1086 64.4857C80.3332 64.4857 79.6949 65.2269 79.2663 65.7976L77.1127 77.9773H73.3848Z\"\n          fill=\"currentColor\"\n        />\n        <path\n          d=\"M92.2682 69.2503L90.6141 70.6764C89.3828 71.7313 88.7341 72.701 88.5939 73.5002C88.4583 74.271 88.7147 74.8405 89.401 74.8405C90.2663 74.8405 90.9936 74.0993 91.5374 73.3854L92.2682 69.2503ZM90.7845 77.9781L91.4952 75.9818H91.1374C90.4887 76.781 89.2722 78.2634 87.5131 78.2634C85.5443 78.2634 84.7028 76.781 85.0609 74.7552C85.444 72.5874 86.6763 71.0197 89.107 69.0792L92.8222 66.1133L92.893 65.7143C93.1244 64.4024 92.8577 63.718 92.0528 63.718C91.248 63.718 90.7384 64.4024 90.5068 65.7143L90.1786 67.5684H86.4505L86.6376 66.5134C87.2224 63.2053 89.4352 61.1511 92.5065 61.1511C95.5779 61.1511 97.0644 63.2042 96.4751 66.5418L94.4526 77.9781H90.7845Z\"\n          fill=\"currentColor\"\n        />\n      </g>\n      <defs>\n        <clipPath id=\"clip0_16329_10574\">\n          <rect\n            fill=\"white\"\n            height=\"20.4\"\n            transform=\"translate(17 58)\"\n            width=\"102\"\n          />\n        </clipPath>\n      </defs>\n    </svg>\n  );\n}\n\nfunction IDEO() {\n  return (\n    <svg\n      aria-hidden=\"true\"\n      fill=\"none\"\n      height=\"136\"\n      preserveAspectRatio=\"xMidYMid meet\"\n      viewBox=\"0 0 136 136\"\n      width=\"136\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <path\n        d=\"M102.877 57.6337C97.0828 57.6337 92.3774 62.2756 92.3774 68.0058C92.3774 73.7361 97.0828 78.378 102.877 78.378C108.671 78.378 113.377 73.7361 113.377 68.0058C113.377 62.2756 108.671 57.6337 102.877 57.6337ZM22.6221 58.4659V61.8272H30.4651V74.2164H22.6221V77.5777H41.7014V74.2164H33.8902V61.8272H41.7014V58.4659H22.6221ZM46.7596 58.4659V77.5452H55.915C61.8373 77.4811 65.9027 73.32 65.9027 67.9739C65.9027 62.6919 61.9014 58.6259 56.0751 58.4659H46.7596ZM70.0643 58.4659V77.5777H89.3362V74.2164H73.5219V69.7021H85.8467V66.3408H73.5219V61.8272H89.3362V58.4659H70.0643ZM102.877 61.2513C106.655 61.2513 109.76 64.2923 109.76 68.0377C109.76 71.7832 106.655 74.8241 102.877 74.8241C99.0996 74.8241 95.9944 71.7832 95.9944 68.0377C95.9944 64.2923 99.0676 61.2513 102.877 61.2513ZM50.1847 61.7634H55.9788C59.7243 61.8594 62.1893 64.4843 62.1893 68.0377C62.1573 71.6231 59.6925 74.2161 55.915 74.2801H50.1847V61.7634Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n}\n\nfunction KhanAcademy() {\n  return (\n    <svg\n      aria-hidden=\"true\"\n      fill=\"none\"\n      height=\"136\"\n      preserveAspectRatio=\"xMidYMid meet\"\n      viewBox=\"0 0 136 136\"\n      width=\"136\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <path\n        clipRule=\"evenodd\"\n        d=\"M1.6155 61.895C1.1767 62.1158 1 62.4707 1 62.9552V72.8385C1 73.3229 1.1767 73.6336 1.6155 73.8987L9.95574 78.6489C10.147 78.7441 10.3576 78.7936 10.5712 78.7936C10.7848 78.7936 10.9955 78.7441 11.1868 78.6489L19.527 73.8987C19.9687 73.6336 20.1425 73.3229 20.1425 72.8385V62.9552C20.1425 62.3824 19.876 62.0717 19.527 61.895L11.1868 57.1447C10.9955 57.0495 10.7848 57 10.5712 57C10.3576 57 10.147 57.0495 9.95574 57.1447L1.6155 61.895ZM17.8943 65.8195C13.9362 65.8401 10.7321 69.1635 10.7321 73.2629V73.4367H10.4243V73.2629C10.4243 69.1635 7.21429 65.8401 3.24885 65.8195C3.22224 66.0798 3.20897 66.3412 3.20909 66.6029C3.20909 69.8085 5.17193 72.5414 7.92109 73.5898C8.76892 73.906 9.66672 74.0671 10.5716 74.0654C11.4762 74.0647 12.3736 73.9037 13.2221 73.5898C15.9698 72.5414 17.9341 69.8085 17.9341 66.6029C17.9342 66.3412 17.9209 66.0798 17.8943 65.8195ZM12.2376 62.2773C13.1576 63.1973 13.1576 64.689 12.2376 65.6091C11.3175 66.5292 9.82576 66.5292 8.90569 65.6091C7.98561 64.689 7.98561 63.1973 8.90569 62.2773C9.82576 61.3572 11.3175 61.3572 12.2376 62.2773Z\"\n        fill=\"currentColor\"\n        fillRule=\"evenodd\"\n      />\n      <path\n        d=\"M25.4532 62.0379C25.4524 61.9812 25.463 61.9251 25.4845 61.8727C25.5059 61.8203 25.5378 61.7728 25.5781 61.733C25.6184 61.6933 25.6663 61.6621 25.719 61.6413C25.7717 61.6206 25.828 61.6107 25.8846 61.6123H27.4425C27.555 61.6135 27.6626 61.6587 27.7422 61.7382C27.8217 61.8178 27.8669 61.9253 27.8681 62.0379V66.819L32.3297 61.789C32.3684 61.7354 32.4189 61.6915 32.4775 61.6608C32.536 61.6302 32.6009 61.6136 32.6669 61.6123H34.4339C34.5089 61.6096 34.583 61.6293 34.6467 61.669C34.7104 61.7087 34.7608 61.7664 34.7915 61.8349C34.8222 61.9034 34.8317 61.9794 34.8189 62.0534C34.8061 62.1273 34.7715 62.1957 34.7196 62.2499L30.093 67.4566L35.0509 73.3878C35.094 73.446 35.12 73.5152 35.1259 73.5874C35.1318 73.6596 35.1174 73.732 35.0843 73.7965C35.0512 73.8609 35.0008 73.9149 34.9387 73.9522C34.8766 73.9895 34.8053 74.0087 34.7329 74.0077H32.8054C32.7464 74.0131 32.687 74.0047 32.6318 73.9833C32.5766 73.9618 32.5271 73.9278 32.4873 73.884L27.8622 68.1825V73.5822C27.861 73.6947 27.8158 73.8022 27.7363 73.8818C27.6567 73.9613 27.5491 74.0065 27.4366 74.0077H25.8846C25.8285 74.0085 25.7728 73.998 25.7208 73.9769C25.6688 73.9558 25.6216 73.9245 25.5819 73.8848C25.5423 73.8452 25.5109 73.7979 25.4898 73.7459C25.4687 73.6939 25.4583 73.6383 25.4591 73.5822L25.4532 62.0379Z\"\n        fill=\"currentColor\"\n      />\n      <path\n        d=\"M36.3398 62.0023C36.3464 61.9009 36.3896 61.8055 36.4615 61.7337C36.5333 61.6619 36.6287 61.6186 36.7301 61.6121H38.1643C38.2663 61.6161 38.3631 61.6585 38.4352 61.7309C38.5073 61.8033 38.5494 61.9002 38.553 62.0023V66.3402C38.9609 65.9853 39.6692 65.6496 40.6425 65.6496C43.2459 65.6496 43.9541 67.4564 43.9541 69.5105V73.6187C43.9505 73.721 43.9082 73.8182 43.8358 73.8906C43.7634 73.963 43.6662 74.0053 43.5639 74.0089H42.1297C42.0267 74.0078 41.9283 73.9662 41.8556 73.8933C41.7829 73.8203 41.7417 73.7217 41.741 73.6187V69.4958C41.741 68.3634 41.2271 67.6728 40.2537 67.6728C39.2804 67.6728 38.765 68.2751 38.553 69.107V73.6187C38.553 73.8499 38.447 74.0089 38.1289 74.0089H36.7301C36.6277 74.0053 36.5306 73.963 36.4582 73.8906C36.3858 73.8182 36.3435 73.721 36.3398 73.6187V62.0023Z\"\n        fill=\"currentColor\"\n      />\n      <path\n        d=\"M48.4549 68.8405C48.989 68.8466 49.5198 68.9239 50.0334 69.0702C50.0688 68.0248 49.7684 67.53 48.9011 67.53C48.1248 67.5409 47.3522 67.6397 46.5981 67.8245C46.3331 67.9129 46.174 67.7185 46.1387 67.4711L45.959 66.5538C45.94 66.504 45.9319 66.4508 45.9352 66.3977C45.9386 66.3445 45.9533 66.2927 45.9785 66.2458C46.0036 66.1989 46.0386 66.1579 46.0809 66.1257C46.1233 66.0934 46.1721 66.0707 46.2241 66.059C47.1334 65.7946 48.0749 65.6574 49.0218 65.6511C51.5722 65.6511 52.1023 66.9764 52.1023 69.2101V73.6217C52.1015 73.7247 52.0604 73.8233 51.9877 73.8962C51.915 73.9692 51.8166 74.0107 51.7136 74.0119H51.1113C50.9641 74.0119 50.8639 73.9589 50.7564 73.7292L50.5267 73.1799C50.1883 73.5132 49.787 73.776 49.3461 73.9529C48.9053 74.1299 48.4337 74.2175 47.9587 74.2107C46.3301 74.2107 45.1963 73.1667 45.1963 71.413C45.1963 69.9552 46.3831 68.8405 48.4549 68.8405ZM48.5079 72.5586C49.2339 72.5586 49.8715 71.9917 49.9804 71.6913V70.4868C49.5981 70.3271 49.1888 70.2417 48.7745 70.235C47.835 70.235 47.3211 70.6767 47.3211 71.4027C47.3211 72.0977 47.7467 72.5586 48.5079 72.5586Z\"\n        fill=\"currentColor\"\n      />\n      <path\n        d=\"M53.9443 66.2189C53.9447 66.1158 53.9858 66.017 54.0585 65.944C54.1313 65.8709 54.23 65.8295 54.3331 65.8287H55.006C55.083 65.8213 55.16 65.8426 55.2223 65.8886C55.2845 65.9345 55.3276 66.0018 55.3432 66.0776L55.6377 66.8389C55.9589 66.4534 56.3638 66.1461 56.8215 65.9405C57.2793 65.7348 57.7778 65.6362 58.2794 65.652C60.8827 65.652 61.5557 67.4043 61.5557 69.3877V73.6211C61.5517 73.7233 61.5093 73.8203 61.437 73.8926C61.3646 73.965 61.2677 74.0073 61.1655 74.0113H59.7313C59.6283 74.0102 59.5298 73.9687 59.4571 73.8957C59.3844 73.8227 59.3433 73.7241 59.3425 73.6211V69.3877C59.3425 68.3261 58.917 67.6708 57.926 67.6708C57.5326 67.6611 57.1468 67.7805 56.8277 68.0108C56.5086 68.2411 56.2737 68.5696 56.159 68.946V73.6211C56.159 73.923 56.0353 74.0113 55.6451 74.0113H54.3345C54.2325 74.0073 54.1357 73.9649 54.0636 73.8925C53.9915 73.8202 53.9494 73.7232 53.9458 73.6211L53.9443 66.2189Z\"\n        fill=\"currentColor\"\n      />\n      <path\n        d=\"M65.7443 73.5521L71.2514 61.6353C71.2759 61.5775 71.3169 61.5281 71.3693 61.4935C71.4217 61.459 71.4832 61.4407 71.5459 61.4409H71.7241C71.7876 61.4372 71.8506 61.4541 71.9037 61.4891C71.9568 61.5242 71.9971 61.5755 72.0186 61.6353L77.4669 73.5521C77.4936 73.6012 77.5067 73.6566 77.5047 73.7124C77.5028 73.7682 77.4859 73.8225 77.4558 73.8696C77.4257 73.9167 77.3836 73.9549 77.3337 73.9801C77.2838 74.0053 77.2281 74.0167 77.1724 74.013H75.6424C75.3774 74.013 75.2537 73.907 75.13 73.6582L74.2612 71.7439H68.9676L68.1003 73.6582C68.0627 73.7637 67.9928 73.8548 67.9006 73.9185C67.8083 73.9821 67.6984 74.0152 67.5864 74.013H66.0462C65.99 74.0176 65.9336 74.007 65.8829 73.9822C65.8322 73.9574 65.7892 73.9194 65.7583 73.8723C65.7274 73.8251 65.7098 73.7705 65.7073 73.7141C65.7049 73.6578 65.7176 73.6018 65.7443 73.5521ZM73.3939 69.7811L71.6269 65.8849H71.5739L69.8349 69.7811H73.3939Z\"\n        fill=\"currentColor\"\n      />\n      <path\n        d=\"M82.0411 65.6528C83.3163 65.6528 84.3073 66.2021 85.0862 67.034C85.2629 67.2107 85.1746 67.4419 84.9979 67.623L84.2543 68.4373C84.0776 68.6317 83.8832 68.5433 83.7227 68.3843C83.3339 68.0118 82.8554 67.676 82.0941 67.676C80.8189 67.676 79.9339 68.667 79.9339 69.9245C79.9339 71.182 80.8012 72.1907 82.0764 72.1907C82.9791 72.1907 83.4576 71.6959 83.811 71.288C83.8748 71.2169 83.9627 71.1721 84.0577 71.1623C84.1528 71.1525 84.248 71.1784 84.3249 71.235L85.1216 71.908C85.3174 72.0847 85.4057 72.279 85.2688 72.497C84.5782 73.5763 83.4812 74.2139 82.0293 74.2139C79.638 74.2139 77.709 72.3909 77.709 69.9289C77.7031 67.5126 79.6144 65.6528 82.0411 65.6528Z\"\n        fill=\"currentColor\"\n      />\n      <path\n        d=\"M89.618 68.8405C90.1511 68.8469 90.6809 68.9242 91.1936 69.0702C91.2289 68.0248 90.9285 67.53 90.0612 67.53C89.2849 67.5409 88.5123 67.6397 87.7582 67.8245C87.4932 67.9129 87.3342 67.7185 87.2988 67.4711L87.1207 66.5538C87.1019 66.5039 87.094 66.4506 87.0976 66.3975C87.1011 66.3444 87.116 66.2926 87.1413 66.2457C87.1665 66.1988 87.2016 66.1579 87.244 66.1257C87.2864 66.0935 87.3352 66.0707 87.3872 66.059C88.2965 65.7946 89.238 65.6574 90.1849 65.6511C92.7353 65.6511 93.2654 66.9764 93.2654 69.2101V73.6217C93.2646 73.7247 93.2235 73.8233 93.1508 73.8962C93.0781 73.9692 92.9797 74.0107 92.8767 74.0119H92.2744C92.1272 74.0119 92.0256 73.9589 91.9195 73.7292L91.6898 73.1799C91.3513 73.5132 90.95 73.776 90.5092 73.9529C90.0684 74.1299 89.5968 74.2175 89.1218 74.2107C87.4932 74.2107 86.3594 73.1667 86.3594 71.413C86.3594 69.9552 87.5462 68.8405 89.618 68.8405ZM89.671 72.5586C90.397 72.5586 91.0346 71.9917 91.1435 71.6913V70.4868C90.7616 70.3274 90.3528 70.2419 89.939 70.235C89.001 70.235 88.4871 70.6767 88.4871 71.4027C88.4842 72.0977 88.9083 72.5586 89.671 72.5586Z\"\n        fill=\"currentColor\"\n      />\n      <path\n        d=\"M98.7096 65.6501C99.3788 65.6533 100.043 65.7667 100.675 65.9858V62.0101C100.682 61.9088 100.725 61.8133 100.797 61.7415C100.869 61.6697 100.964 61.6265 101.066 61.6199H102.5C102.602 61.6239 102.699 61.6663 102.771 61.7387C102.843 61.8111 102.885 61.908 102.889 62.0101V73.6192C102.888 73.7222 102.847 73.8208 102.774 73.8937C102.701 73.9667 102.603 74.0082 102.5 74.0094H101.845C101.649 74.0094 101.525 73.8503 101.454 73.6192L101.242 72.9816C100.911 73.3661 100.5 73.6747 100.039 73.8864C99.5775 74.098 99.0759 74.2078 98.5682 74.2082C96.3374 74.2082 94.6514 72.397 94.6514 69.9173C94.6514 67.5275 96.2711 65.6501 98.7096 65.6501ZM100.675 68.1297C100.162 67.8222 99.5742 67.6627 98.9761 67.6688C97.6302 67.6688 96.9396 68.7659 96.9396 69.9173C96.9396 71.0688 97.5949 72.1835 98.8686 72.1835C99.9317 72.1835 100.499 71.5106 100.675 71.0511V68.1297Z\"\n        fill=\"currentColor\"\n      />\n      <path\n        d=\"M108.431 65.6526C110.555 65.6526 112.184 67.2281 112.184 69.4413C112.184 69.5665 112.166 69.8492 112.149 69.9729C112.138 70.0709 112.094 70.1621 112.023 70.2303C111.951 70.2986 111.858 70.3393 111.76 70.3454H106.518C106.536 71.3541 107.367 72.239 108.554 72.239C109.191 72.2504 109.812 72.0378 110.308 71.6382C110.502 71.4777 110.714 71.4601 110.838 71.6382L111.529 72.5586C111.566 72.5933 111.595 72.6359 111.614 72.6833C111.633 72.7308 111.641 72.7818 111.638 72.8328C111.634 72.8837 111.62 72.9332 111.595 72.9777C111.57 73.0223 111.535 73.0607 111.493 73.0901C110.647 73.8192 109.565 74.2173 108.448 74.2107C106.004 74.2107 104.305 72.2626 104.305 69.9257C104.305 67.6184 106.004 65.6526 108.431 65.6526ZM109.953 68.8921C109.918 68.094 109.28 67.4755 108.378 67.4755C107.385 67.4755 106.749 68.0763 106.642 68.8921H109.953Z\"\n        fill=\"currentColor\"\n      />\n      <path\n        d=\"M113.499 66.2193C113.499 66.1162 113.54 66.0174 113.613 65.9443C113.686 65.8713 113.785 65.8299 113.888 65.8291H114.525C114.6 65.8216 114.675 65.8406 114.736 65.883C114.798 65.9253 114.843 65.9881 114.863 66.0603L115.157 66.8392C115.454 66.4615 115.835 66.1577 116.269 65.9518C116.704 65.7459 117.18 65.6434 117.66 65.6524C118.847 65.6524 119.427 66.0779 120.103 66.8922C120.458 66.5197 121.272 65.6524 122.742 65.6524C125.363 65.6524 125.981 67.3163 125.981 69.4411V73.6215C125.98 73.674 125.969 73.7257 125.948 73.7738C125.927 73.8218 125.897 73.8653 125.859 73.9015C125.821 73.9378 125.776 73.9662 125.727 73.9851C125.678 74.004 125.626 74.0131 125.574 74.0117H124.157C124.054 74.0105 123.956 73.969 123.883 73.8961C123.81 73.8231 123.769 73.7245 123.768 73.6215V69.3881C123.768 68.3264 123.378 67.6712 122.352 67.6712C121.165 67.6712 120.828 68.5208 120.828 68.5208C120.828 68.5208 120.863 68.9287 120.863 69.2997V73.6215C120.859 73.7236 120.817 73.8205 120.745 73.8929C120.673 73.9652 120.576 74.0077 120.474 74.0117H119.022C118.971 74.0129 118.92 74.0036 118.872 73.9845C118.824 73.9653 118.78 73.9366 118.744 73.9001C118.707 73.8636 118.679 73.8201 118.659 73.7722C118.64 73.7244 118.631 73.6731 118.632 73.6215V69.3881C118.632 68.3264 118.338 67.6712 117.286 67.6712C116.278 67.6712 115.905 68.3794 115.711 68.9463V73.6215C115.707 73.7236 115.665 73.8205 115.593 73.8929C115.521 73.9652 115.424 74.0077 115.322 74.0117H113.886C113.784 74.0073 113.688 73.9647 113.616 73.8924C113.545 73.8201 113.503 73.7233 113.499 73.6215V66.2193Z\"\n        fill=\"currentColor\"\n      />\n      <path\n        d=\"M126.828 66.3567C126.722 66.074 126.863 65.8252 127.181 65.8252H128.882C128.956 65.818 129.031 65.8372 129.092 65.8795C129.153 65.9218 129.198 65.9844 129.218 66.0563L130.69 70.9435H130.727L132.582 66.0563C132.706 65.8428 132.847 65.8252 133.096 65.8252H134.601C134.667 65.8179 134.733 65.8289 134.793 65.8568C134.852 65.8847 134.903 65.9285 134.94 65.9834C134.976 66.0383 134.997 66.1021 134.999 66.168C135.002 66.2338 134.986 66.2991 134.954 66.3567L130.138 77.9732C130.107 78.0429 130.058 78.1029 129.995 78.1465C129.933 78.1901 129.859 78.2157 129.783 78.2205H128.085C128.018 78.2265 127.951 78.214 127.89 78.1845C127.83 78.155 127.779 78.1095 127.742 78.053C127.706 77.9965 127.685 77.931 127.683 77.8638C127.681 77.7965 127.697 77.73 127.73 77.6713L129.377 73.5277L126.828 66.3567Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n}\n\nfunction Quizlet() {\n  return (\n    <svg\n      aria-hidden=\"true\"\n      fill=\"none\"\n      height=\"136\"\n      preserveAspectRatio=\"xMidYMid meet\"\n      viewBox=\"0 0 136 136\"\n      width=\"136\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <g clipPath=\"url(#clip0_16329_10577)\">\n        <path\n          clipRule=\"evenodd\"\n          d=\"M33.7341 59.391C39.2817 59.391 43.4682 63.4635 43.4682 68.6698C43.4682 71.0671 42.5521 73.1807 41.0605 74.7785L43.6507 77.6393H39.281L38.383 76.6186C37.0846 77.5084 35.4609 77.9228 33.7341 77.9228C28.2125 77.9228 24 73.8762 24 68.6702C24 63.3082 28.3697 59.391 33.7341 59.391ZM33.7341 74.3401C34.4662 74.3401 35.1205 74.1859 35.7487 73.9278L32.0066 69.7525H36.3766L38.3916 72.0468C39.1237 71.1441 39.4642 70.0618 39.4642 68.6698C39.4642 65.5517 37.0839 62.9996 33.7334 62.9996C30.384 62.9996 28.03 65.5255 28.03 68.6698C28.03 71.8658 30.384 74.3404 33.7337 74.3404L33.7341 74.3401ZM46.0126 64.6501H49.9636V72.073C49.9636 73.826 51.0629 74.4698 52.3713 74.4698C53.6794 74.4698 54.7787 73.8256 54.7787 72.073V64.6494H58.7301V72.4071C58.7297 76.2221 55.7471 78.0003 52.3713 78.0003C48.9956 78.0003 46.013 76.2221 46.013 72.4078V64.649L46.0126 64.6501ZM61.4307 77.64H65.3293V64.6494H61.4307V77.6393V77.64ZM61.0729 61.2605C61.0729 59.9843 62.126 59.001 63.3667 59.001C64.6354 59.001 65.6604 59.9843 65.6604 61.2605C65.6604 62.5099 64.6354 63.4939 63.3667 63.4939C62.126 63.4939 61.0729 62.5099 61.0729 61.2605ZM73.962 68.0263H67.9246V64.6501H80.6673L73.4001 74.2637H80.3005V77.64H66.6172L73.962 68.0263ZM82.4987 77.64H86.3974V59.6753H82.4984V77.6393L82.4987 77.64ZM88.6526 71.119C88.6526 66.9953 91.6359 64.3407 95.6392 64.3407C99.6688 64.3407 102.311 67.2789 102.311 70.9133C102.311 70.9133 102.311 71.641 102.233 72.2339H92.552C92.6306 73.7805 93.8071 74.7014 96.0316 74.7014C98.5439 74.7014 99.9566 73.954 100.715 73.4385V76.7893C99.4856 77.5625 98.1252 77.9752 95.8224 77.9752C91.3997 77.9752 88.6526 75.3205 88.6526 71.2481V71.119ZM98.3867 69.6757C98.3867 68.516 97.1832 67.5366 95.6392 67.5366C94.017 67.5366 92.6566 68.4902 92.578 69.6757H98.3867ZM105.301 68.1296H103.548V64.6501H105.301V59.6749H109.095V64.6494H112V68.1289H109.095V77.6396H105.301V68.1289V68.1296Z\"\n          fill=\"currentColor\"\n          fillRule=\"evenodd\"\n        />\n      </g>\n      <defs>\n        <clipPath id=\"clip0_16329_10577\">\n          <rect\n            fill=\"white\"\n            height=\"19\"\n            transform=\"translate(24 59)\"\n            width=\"88\"\n          />\n        </clipPath>\n      </defs>\n    </svg>\n  );\n}\n\nfunction Ramp() {\n  return (\n    <svg\n      aria-hidden=\"true\"\n      fill=\"none\"\n      height=\"136\"\n      preserveAspectRatio=\"xMidYMid meet\"\n      viewBox=\"0 0 136 136\"\n      width=\"136\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <path\n        d=\"M110.286 55C110.481 55.0979 110.579 55.2937 110.775 55.3916C111.754 56.3706 112.708 57.3251 113.59 58.2061C113.492 61.0207 112.537 63.6149 111.167 66.0379C108.842 70.0027 105.073 72.9885 100.618 74.3591C99.37 74.7507 97.9995 75.0444 96.7513 75.0444C95.7723 74.0654 94.9157 73.2088 93.9367 72.2298C93.741 72.034 93.5452 71.9361 93.4473 71.6425C97.51 71.3488 101.475 69.6111 104.363 66.7966C106.394 64.8631 108.132 62.4402 109.111 59.8459C109.796 58.4019 110.188 56.7377 110.286 55Z\"\n        fill=\"currentColor\"\n      />\n      <path\n        d=\"M24.9927 61.2898C25.5801 60.7024 26.3388 60.2374 27.2199 60.2374C28.0031 60.1395 28.8597 60.2374 29.5449 60.7268C29.0555 61.7058 28.566 62.7582 28.101 63.7127C27.122 63.3211 25.9717 63.3211 25.1151 63.9085C24.5277 64.3001 24.0627 64.8875 23.769 65.5483C23.4753 66.3314 23.2795 67.188 23.2795 67.9712C23.2795 70.3942 23.2795 72.7192 23.2795 75.1177C22.2272 75.1177 21.0524 75.1177 20 75.1177C20 70.2718 20 65.4504 20 60.6045C21.0524 60.6045 22.1293 60.6045 23.1816 60.6045C23.1816 61.8527 23.1816 63.1253 23.1816 64.3735C23.6467 63.3211 24.1361 62.1708 24.9927 61.2898Z\"\n        fill=\"currentColor\"\n      />\n      <path\n        d=\"M32.4572 61.0939C33.7053 60.2128 35.3696 60.1149 36.8136 60.2128C37.9638 60.3107 39.2365 60.6044 40.191 61.3631C41.0721 62.0484 41.635 63.1987 41.8308 64.2755C42.0266 64.9608 42.0266 65.6216 42.0266 66.4048C42.0266 69.3172 42.0266 72.2052 42.0266 75.1176C40.9742 75.1176 39.8973 75.1176 38.8449 75.1176C38.8449 74.3344 38.8449 73.5757 38.8449 72.6947C38.5512 73.2821 38.1596 73.9429 37.6946 74.3344C36.8136 75.0197 35.7612 75.3868 34.7088 75.3868C33.3627 75.4847 31.8942 75.191 30.8419 74.4079C29.9608 73.8205 29.3979 72.7681 29.2021 71.6912C29.1042 70.8102 29.2021 69.8557 29.6916 68.9746C30.0832 68.2893 30.6705 67.8243 31.3313 67.4327C32.0166 67.0411 32.8732 66.8453 33.6564 66.6495C35.0025 66.3559 36.373 66.3559 37.6212 65.9643C38.1107 65.8664 38.6002 65.5727 38.7715 65.1811C39.0652 64.5937 38.9673 63.9329 38.5757 63.4434C38.1841 62.9539 37.5967 62.7581 37.0338 62.6603C36.1528 62.4645 35.2962 62.5624 34.513 62.9539C33.7298 63.3455 33.2648 64.1042 33.069 64.9853C32.1879 64.5937 31.2334 64.3 30.3524 64.0063C30.5971 62.9295 31.3803 61.7792 32.4572 61.0939ZM38.0617 67.5796C37.3765 68.069 36.5199 68.2648 35.6388 68.4606C34.8556 68.5585 34.1948 68.7543 33.5095 69.1459C33.0201 69.4396 32.5306 69.9291 32.4572 70.5899C32.3593 71.2752 32.4572 71.936 32.8487 72.5234C33.2403 73.0128 33.7298 73.2086 34.2927 73.3065C35.2717 73.5023 36.422 73.3065 37.2051 72.6212C37.9883 72.0339 38.4533 71.1773 38.6491 70.1983C38.9428 69.048 38.8449 67.7754 38.8449 66.5272C38.7225 66.8943 38.4289 67.2859 38.0617 67.5796Z\"\n        fill=\"currentColor\"\n      />\n      <path\n        d=\"M50.1527 60.9962C51.205 60.1151 52.6735 60.0172 53.9217 60.3109C54.9741 60.5067 55.953 61.2899 56.4425 62.2444C56.7362 62.636 56.8341 63.1255 57.0299 63.5905C57.3236 62.5381 57.911 61.5591 58.7676 60.8738C59.7465 60.1886 61.0926 60.0907 62.2429 60.2865C63.3932 60.4823 64.4701 61.0696 65.1553 62.0241C65.7427 62.8073 66.0364 63.7618 66.1343 64.7408C66.2322 65.2303 66.1343 65.7197 66.1343 66.2826C66.1343 69.2685 66.1343 72.1809 66.1343 75.1668C65.0819 75.1668 64.005 75.1668 62.9527 75.1668C62.9527 72.2543 62.9527 69.4643 62.9527 66.5519C62.9527 65.5729 62.8548 64.6184 62.2674 63.8352C61.8758 63.2478 61.215 62.9542 60.5297 62.9542C59.6486 62.8563 58.792 63.1499 58.2047 63.8352C57.4215 64.8142 57.3236 66.0624 57.2257 67.2127C57.2257 69.8314 57.2257 72.5236 57.2257 75.1423C56.1733 75.1423 55.0964 75.1423 54.044 75.1423C54.044 72.3278 54.044 69.5377 54.044 66.7232C54.044 65.8421 53.9462 64.9855 53.6525 64.2023C53.3588 63.615 52.8693 63.1499 52.3064 63.052C51.4253 62.8563 50.4708 62.9542 49.7855 63.4436C49.1982 63.9331 48.9045 64.5939 48.7331 65.2792C48.5374 65.9645 48.4395 66.6253 48.4395 67.3106C48.4395 69.9293 48.4395 72.5236 48.4395 75.1423C47.3871 75.1423 46.3102 75.1423 45.2578 75.1423C45.2578 70.2964 45.2578 65.475 45.2578 60.6291C46.3102 60.6291 47.3871 60.6291 48.4395 60.6291C48.4395 61.6815 48.4395 62.8563 48.4395 63.9086C48.6842 62.7339 49.1737 61.6815 50.1527 60.9962Z\"\n        fill=\"currentColor\"\n      />\n      <path\n        d=\"M74.6019 60.996C75.5809 60.3107 76.927 60.1149 78.0773 60.2128C79.4233 60.3107 80.7939 60.996 81.7484 61.9505C82.8008 63.0029 83.486 64.3735 83.7797 65.8174C84.0734 67.2614 84.0734 68.7299 83.6818 70.0759C83.3881 71.422 82.7029 72.7926 81.6505 73.845C80.5981 74.8239 79.2275 75.3868 77.8815 75.4847C76.6333 75.5826 75.2627 75.3868 74.3082 74.5058C73.5251 73.9184 72.9621 72.9639 72.6685 71.9849C72.6685 74.8974 72.6685 77.6874 72.6685 80.5999C71.5182 80.5999 70.4413 80.5999 69.291 80.5999C69.291 73.9184 69.291 67.1635 69.291 60.4821C70.4413 60.4821 71.5182 60.4821 72.6685 60.4821C72.6685 61.461 72.6685 62.4155 72.6685 63.3945C73.06 62.5379 73.6474 61.5834 74.6019 60.996ZM75.7767 62.9295C75.3851 63.0274 74.9935 63.1253 74.6264 63.419C73.7453 63.9084 73.0845 64.8629 72.7908 65.8419C72.3992 66.9922 72.3992 68.2649 72.6929 69.513C72.8887 70.6633 73.574 71.7402 74.5285 72.3276C75.5075 72.915 76.6577 73.0128 77.7101 72.7192C78.7625 72.4255 79.6436 71.6668 80.1331 70.6878C80.8184 69.4396 80.9163 67.9712 80.7205 66.6251C80.5247 65.4748 79.9373 64.3 78.9828 63.5413C77.9793 62.9295 76.8291 62.7337 75.7767 62.9295Z\"\n        fill=\"currentColor\"\n      />\n      <path\n        d=\"M105.538 74.0654C106.517 73.3801 107.471 72.6214 108.352 71.7404C110.09 71.7404 111.828 71.7404 113.565 71.7404C114.618 72.7927 115.792 73.9675 116.845 75.0199C116.16 75.1178 115.401 75.0199 114.716 75.0199C111.436 75.0199 108.034 75.0199 104.755 75.0199C104.559 75.0199 104.265 75.0199 104.069 75.0199C104.485 74.7262 105.073 74.4325 105.538 74.0654Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n}\n\nfunction Strava() {\n  return (\n    <svg\n      aria-hidden=\"true\"\n      fill=\"none\"\n      height=\"136\"\n      preserveAspectRatio=\"xMidYMid meet\"\n      viewBox=\"0 0 136 136\"\n      width=\"136\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <g clipPath=\"url(#clip0_16329_10564)\">\n        <path\n          d=\"M36.5551 69C36.9106 69.6222 37.1106 70.4 37.1106 71.3111V71.3555C37.1106 72.2889 36.9328 73.1333 36.5551 73.8889C36.1773 74.6444 35.6439 75.2666 34.9773 75.8C34.2884 76.3111 33.4662 76.7111 32.4884 77C31.5106 77.2889 30.4217 77.4222 29.2217 77.4222C27.3995 77.4222 25.6884 77.1778 24.1106 76.6666C22.5328 76.1555 21.1773 75.4 20.0439 74.4L23.2439 70.6C24.2217 71.3555 25.2439 71.8889 26.3106 72.2C27.3773 72.5333 28.4439 72.6889 29.5106 72.6889C30.0662 72.6889 30.4439 72.6222 30.6884 72.4889C30.9328 72.3555 31.0439 72.1555 31.0439 71.9333V71.8889C31.0439 71.6222 30.8662 71.4222 30.5106 71.2444C30.1551 71.0667 29.5106 70.8889 28.5551 70.7111C27.5551 70.5111 26.5995 70.2667 25.6884 70C24.7773 69.7333 23.9773 69.3778 23.2884 68.9555C22.5995 68.5333 22.0439 68 21.6439 67.3555C21.1995 66.6667 20.9995 65.8667 20.9995 64.9333V64.8889C20.9995 64.0444 21.1551 63.2444 21.4884 62.5111C21.8217 61.7778 22.3106 61.1333 22.9551 60.6C23.5995 60.0444 24.3995 59.6222 25.3328 59.3111C26.2662 59 27.3551 58.8444 28.5995 58.8444C30.3328 58.8444 31.8662 59.0444 33.1551 59.4667C34.4662 59.8667 35.6217 60.4889 36.6662 61.3111L33.7551 65.3333C32.9106 64.7111 31.9995 64.2667 31.0662 63.9778C30.1106 63.6889 29.2217 63.5555 28.3995 63.5555C27.9551 63.5555 27.6217 63.6222 27.4217 63.7555C27.1995 63.8889 27.1106 64.0667 27.1106 64.2889V64.3333C27.1106 64.5778 27.2662 64.7778 27.5995 64.9555C27.9328 65.1333 28.5551 65.3111 29.4884 65.4889C30.6217 65.6889 31.6662 65.9333 32.5995 66.2222C33.5328 66.5111 34.3328 66.8889 35.0217 67.3333C35.6662 67.8222 36.1995 68.3555 36.5551 69ZM36.7773 64.2444H42.0439V77.0889H48.0217V64.2444H53.2884V59.1778H36.7773V64.2444ZM106.199 58.0667L96.5773 77.0889H102.311L106.199 69.4L110.111 77.0889H115.844L106.199 58.0667ZM79.3995 58.0667L89.0439 77.0889H83.3106L79.4217 69.4L75.5328 77.0889H71.6439H69.7995H64.8217L61.4439 71.9778H61.3995H60.1773V77.0889H54.1995V59.1778H62.8884C64.4884 59.1778 65.7995 59.3555 66.8439 59.7333C67.8662 60.0889 68.7106 60.6 69.3328 61.2222C69.8884 61.7555 70.2884 62.3778 70.5551 63.0667C70.8217 63.7555 70.9551 64.5555 70.9551 65.4667V65.5111C70.9551 66.8222 70.6439 67.9333 69.9995 68.8222C69.3773 69.7333 68.5106 70.4444 67.4217 70.9778L70.5328 75.5111L79.3995 58.0667ZM64.9995 65.9111C64.9995 65.3333 64.7995 64.9111 64.3773 64.6222C63.9773 64.3333 63.4217 64.2 62.7106 64.2H60.1106V67.7111H62.6884C63.3995 67.7111 63.9773 67.5555 64.3773 67.2444C64.7773 66.9333 64.9995 66.5111 64.9995 65.9555V65.9111ZM96.7106 59.1778L92.7995 66.8889L88.8884 59.1778H83.1551L92.7995 78.2L102.422 59.1778H96.7106Z\"\n          fill=\"currentColor\"\n        />\n      </g>\n      <defs>\n        <clipPath id=\"clip0_16329_10564\">\n          <rect\n            fill=\"white\"\n            height=\"20.2222\"\n            transform=\"translate(20 58)\"\n            width=\"96\"\n          />\n        </clipPath>\n      </defs>\n    </svg>\n  );\n}\n\nexport {\n  Canpoy,\n  Canva,\n  Casetext,\n  Clearbit,\n  Descript,\n  Duolingo,\n  Faire,\n  IDEO,\n  KhanAcademy,\n  Quizlet,\n  Ramp,\n  Strava,\n};\n","path":"logos.tsx","target":"components/smoothui/shared/logos.tsx","type":"registry:component"},{"content":"import { Slot } from \"@radix-ui/react-slot\";\nimport { cn } from \"@/lib/utils\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport type React from \"react\";\nimport type { ButtonHTMLAttributes } from \"react\";\n\nconst buttonVariants = cva(\n  \"inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md font-medium text-sm ring-offset-background transition-transform duration-150 ease-out focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 active:scale-[0.97] disabled:pointer-events-none disabled:opacity-50\",\n  {\n    defaultVariants: {\n      size: \"default\",\n      variant: \"default\",\n    },\n    variants: {\n      size: {\n        default: \"h-10 px-4 py-2\",\n        icon: \"h-10 w-10\",\n        lg: \"h-11 rounded-md px-8\",\n        sm: \"h-9 rounded-md px-4 py-2\",\n      },\n      variant: {\n        candy:\n          \"border-[0.5px] border-white/25 bg-gradient-to-b from-brand to-brand-secondary text-shadow-sm text-white shadow-black/20 shadow-md ring-(--ring-color) ring-1 [--ring-color:color-mix(in_oklab,var(--color-foreground)15%,var(--color-brand))] hover:from-brand-secondary hover:to-brand-secondary [&_svg]:drop-shadow-sm\",\n        default:\n          \"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90\",\n        destructive:\n          \"bg-gradient-to-b from-[#FD4B4E] to-destructive text-shadow-sm text-white shadow-[0px_1px_2px_rgba(0,0,0,0.4),0px_0px_0px_1px_#F61418,inset_0px_0.75px_0px_rgba(255,255,255,0.2)] hover:from-destructive hover:to-destructive\",\n        ghost: \"hover:bg-background hover:text-foreground hover:shadow-custom\",\n        link: \"text-foreground underline-offset-4 hover:underline\",\n        outline:\n          \"border border-transparent bg-background shadow-black/15 shadow-sm ring-1 ring-foreground/10 hover:bg-primary dark:ring-foreground/15\",\n        secondary:\n          \"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80\",\n      },\n    },\n  }\n);\n\nexport interface ButtonProps\n  extends ButtonHTMLAttributes<HTMLButtonElement>,\n    VariantProps<typeof buttonVariants> {\n  asChild?: boolean;\n  ref?: React.Ref<HTMLButtonElement>;\n}\n\nfunction Button({\n  className,\n  variant,\n  size,\n  asChild = false,\n  ref,\n  ...props\n}: ButtonProps) {\n  const Comp = asChild ? Slot : \"button\";\n  return (\n    <Comp\n      className={cn(buttonVariants({ className, size, variant }))}\n      ref={ref}\n      {...props}\n    />\n  );\n}\n\nexport { Button, buttonVariants };\n","path":"smoothbutton.tsx","target":"components/smoothui/shared/smoothbutton.tsx","type":"registry:component"}],"name":"shared","registryDependencies":["https://smoothui.dev/r/smooth-button.json","https://smoothui.dev/r/tokens.json"],"title":"Shared","type":"registry:component"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"Grid stats section block with hover effects","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { motion, useInView, useReducedMotion } from \"motion/react\";\nimport { useRef } from \"react\";\n\nconst STAGGER_DELAY = 0.1;\nconst VALUE_DELAY_OFFSET = 0.2;\n\ninterface StatsGridProps {\n  description?: string;\n  stats?: Array<{\n    value: string;\n    label: string;\n    description?: string;\n  }>;\n  title?: string;\n}\n\nexport function StatsGrid({\n  title = \"Our Impact in Numbers\",\n  description = \"See how we're making a difference across the globe\",\n  stats = [\n    {\n      description: \"Growing every day\",\n      label: \"Active Users\",\n      value: \"10M+\",\n    },\n    {\n      description: \"Reliable service\",\n      label: \"Uptime\",\n      value: \"99.9%\",\n    },\n    {\n      description: \"Worldwide reach\",\n      label: \"Countries\",\n      value: \"150+\",\n    },\n    {\n      description: \"Always here to help\",\n      label: \"Support\",\n      value: \"24/7\",\n    },\n  ],\n}: StatsGridProps) {\n  const ref = useRef(null);\n  const isInView = useInView(ref, { once: true });\n  const shouldReduceMotion = useReducedMotion();\n\n  return (\n    <section className=\"py-20\">\n      <div className=\"mx-auto max-w-7xl px-6\">\n        <motion.div\n          className=\"mb-16 text-center\"\n          initial={shouldReduceMotion ? { opacity: 1 } : { opacity: 0, y: 20 }}\n          transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.6 }}\n          viewport={{ once: true }}\n          whileInView={\n            shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }\n          }\n        >\n          <h2 className=\"mb-4 font-bold text-3xl text-foreground lg:text-4xl\">\n            {title}\n          </h2>\n          <p className=\"mx-auto max-w-2xl text-foreground/70 text-lg\">\n            {description}\n          </p>\n        </motion.div>\n        <div\n          className=\"grid grid-cols-1 gap-8 md:grid-cols-2 lg:grid-cols-4\"\n          ref={ref}\n        >\n          {stats.map((stat, index) => (\n            <motion.div\n              animate={(() => {\n                if (shouldReduceMotion) {\n                  return { opacity: 1 };\n                }\n                return isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 30 };\n              })()}\n              className=\"group relative overflow-hidden rounded-2xl border border-border bg-background p-8 text-center transition-all hover:border-brand hover:shadow-lg\"\n              initial={\n                shouldReduceMotion ? { opacity: 1 } : { opacity: 0, y: 30 }\n              }\n              key={stat.label}\n              transition={\n                shouldReduceMotion\n                  ? { duration: 0 }\n                  : { delay: index * STAGGER_DELAY, duration: 0.6 }\n              }\n            >\n              <motion.div\n                animate={(() => {\n                  if (shouldReduceMotion) {\n                    return { scale: 1 };\n                  }\n                  return isInView ? { scale: 1 } : { scale: 0.5 };\n                })()}\n                className=\"mb-2 font-bold text-4xl text-brand lg:text-5xl\"\n                initial={shouldReduceMotion ? { scale: 1 } : { scale: 0.5 }}\n                transition={\n                  shouldReduceMotion\n                    ? { duration: 0 }\n                    : {\n                        delay: index * STAGGER_DELAY + VALUE_DELAY_OFFSET,\n                        duration: 0.8,\n                        stiffness: 200,\n                        type: \"spring\" as const,\n                      }\n                }\n              >\n                {stat.value}\n              </motion.div>\n              <h3 className=\"mb-2 font-semibold text-foreground text-lg\">\n                {stat.label}\n              </h3>\n              {stat.description ? (\n                <p className=\"text-foreground/70 text-sm\">{stat.description}</p>\n              ) : null}\n              {/* Hover effect background */}\n              <motion.div\n                className=\"absolute inset-0 bg-gradient-to-br from-brand/5 to-transparent opacity-0 group-hover:opacity-100\"\n                initial={{ opacity: 0 }}\n                transition={{ duration: 0.3 }}\n                whileHover={{ opacity: 1 }}\n              />\n            </motion.div>\n          ))}\n        </div>\n      </div>\n    </section>\n  );\n}\n\nexport default StatsGrid;\n","path":"index.tsx","target":"components/smoothui/stats-1/index.tsx","type":"registry:block"}],"name":"stats-1","registryDependencies":["https://smoothui.dev/r/tokens.json"],"title":"Stats 1","type":"registry:block"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["lucide-react","motion"],"description":"Cards stats section block with icons and trends","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { DollarSign, Smartphone, Star, Users } from \"lucide-react\";\nimport { motion, useInView, useReducedMotion } from \"motion/react\";\nimport React, { useRef } from \"react\";\n\nconst STAGGER_DELAY = 0.1;\nconst ICON_STAGGER_OFFSET = 0.2;\nconst VALUE_DELAY_OFFSET = 0.3;\nconst TREND_STAGGER_OFFSET = 0.5;\n\ninterface StatsCardsProps {\n  description?: string;\n  stats?: Array<{\n    value: string;\n    label: string;\n    description?: string;\n    icon?: string;\n    trend?: {\n      value: string;\n      direction: \"up\" | \"down\";\n    };\n  }>;\n  title?: string;\n}\n\nconst iconMap = {\n  DollarSign,\n  Smartphone,\n  Star,\n  Users,\n};\n\nexport function StatsCards({\n  title = \"Key Metrics\",\n  description = \"Track your success with these important numbers\",\n  stats = [\n    {\n      description: \"Annual recurring revenue\",\n      icon: \"DollarSign\",\n      label: \"Revenue\",\n      trend: { direction: \"up\", value: \"+12%\" },\n      value: \"2.5M\",\n    },\n    {\n      description: \"Happy customers worldwide\",\n      icon: \"Users\",\n      label: \"Customers\",\n      trend: { direction: \"up\", value: \"+8%\" },\n      value: \"45K\",\n    },\n    {\n      description: \"Customer satisfaction rate\",\n      icon: \"Star\",\n      label: \"Satisfaction\",\n      trend: { direction: \"up\", value: \"+2%\" },\n      value: \"98%\",\n    },\n    {\n      description: \"Total app downloads\",\n      icon: \"Smartphone\",\n      label: \"Downloads\",\n      trend: { direction: \"up\", value: \"+15%\" },\n      value: \"1.2M\",\n    },\n  ],\n}: StatsCardsProps) {\n  const ref = useRef(null);\n  const isInView = useInView(ref, { once: true });\n  const shouldReduceMotion = useReducedMotion();\n\n  return (\n    <section className=\"py-20\">\n      <div className=\"mx-auto max-w-7xl px-6\">\n        <motion.div\n          className=\"mb-16 text-center\"\n          initial={shouldReduceMotion ? { opacity: 1 } : { opacity: 0, y: 20 }}\n          transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.6 }}\n          viewport={{ once: true }}\n          whileInView={\n            shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }\n          }\n        >\n          <h2 className=\"mb-4 font-bold text-3xl text-foreground lg:text-4xl\">\n            {title}\n          </h2>\n          <p className=\"mx-auto max-w-2xl text-foreground/70 text-lg\">\n            {description}\n          </p>\n        </motion.div>\n\n        <div\n          className=\"grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4\"\n          ref={ref}\n        >\n          {stats.map((stat, index) => (\n            <motion.div\n              animate={(() => {\n                if (shouldReduceMotion) {\n                  return { opacity: 1 };\n                }\n                return isInView\n                  ? { opacity: 1, scale: 1, y: 0 }\n                  : { opacity: 0, scale: 0.9, y: 30 };\n              })()}\n              className=\"group relative overflow-hidden rounded-2xl border border-border bg-gradient-to-br from-background to-background/50 p-6 transition-all hover:scale-105 hover:border-brand hover:shadow-xl\"\n              initial={\n                shouldReduceMotion\n                  ? { opacity: 1 }\n                  : { opacity: 0, scale: 0.9, y: 30 }\n              }\n              key={stat.label}\n              transition={\n                shouldReduceMotion\n                  ? { duration: 0 }\n                  : {\n                      delay: index * STAGGER_DELAY,\n                      duration: 0.6,\n                      stiffness: 100,\n                      type: \"spring\" as const,\n                    }\n              }\n            >\n              {/* Icon */}\n              <motion.div\n                animate={(() => {\n                  if (shouldReduceMotion) {\n                    return { rotate: 0, scale: 1 };\n                  }\n                  return isInView\n                    ? { rotate: 0, scale: 1 }\n                    : { rotate: -10, scale: 0.8 };\n                })()}\n                className=\"mb-4 text-3xl\"\n                initial={\n                  shouldReduceMotion\n                    ? { rotate: 0, scale: 1 }\n                    : { rotate: -10, scale: 0.8 }\n                }\n                transition={\n                  shouldReduceMotion\n                    ? { duration: 0 }\n                    : {\n                        delay: index * STAGGER_DELAY + ICON_STAGGER_OFFSET,\n                        duration: 0.6,\n                        stiffness: 200,\n                        type: \"spring\" as const,\n                      }\n                }\n              >\n                {React.createElement(\n                  iconMap[stat.icon as keyof typeof iconMap] || DollarSign,\n                  {\n                    className: \"h-8 w-8\",\n                  }\n                )}\n              </motion.div>\n\n              {/* Value */}\n              <motion.div\n                animate={(() => {\n                  if (shouldReduceMotion) {\n                    return { scale: 1 };\n                  }\n                  return isInView ? { scale: 1 } : { scale: 0.5 };\n                })()}\n                className=\"mb-1 font-bold text-2xl text-foreground lg:text-3xl\"\n                initial={shouldReduceMotion ? { scale: 1 } : { scale: 0.5 }}\n                transition={\n                  shouldReduceMotion\n                    ? { duration: 0 }\n                    : {\n                        delay: index * STAGGER_DELAY + VALUE_DELAY_OFFSET,\n                        duration: 0.8,\n                        stiffness: 200,\n                        type: \"spring\" as const,\n                      }\n                }\n              >\n                {stat.value}\n              </motion.div>\n\n              {/* Label */}\n              <h3 className=\"mb-2 font-semibold text-foreground text-sm uppercase tracking-wide\">\n                {stat.label}\n              </h3>\n\n              {/* Description */}\n              {stat.description ? (\n                <p className=\"mb-3 text-foreground/70 text-xs\">\n                  {stat.description}\n                </p>\n              ) : null}\n\n              {/* Trend */}\n              {stat.trend ? (\n                <motion.div\n                  animate={(() => {\n                    if (shouldReduceMotion) {\n                      return { opacity: 1 };\n                    }\n                    return isInView\n                      ? { opacity: 1, x: 0 }\n                      : { opacity: 0, x: -10 };\n                  })()}\n                  className={`inline-flex items-center rounded-full px-2 py-1 font-medium text-xs ${\n                    stat.trend.direction === \"up\"\n                      ? \"bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-400\"\n                      : \"bg-red-100 text-red-800 dark:bg-red-900/20 dark:text-red-400\"\n                  }`}\n                  initial={\n                    shouldReduceMotion ? { opacity: 1 } : { opacity: 0, x: -10 }\n                  }\n                  transition={\n                    shouldReduceMotion\n                      ? { duration: 0 }\n                      : {\n                          delay: index * STAGGER_DELAY + TREND_STAGGER_OFFSET,\n                          duration: 0.4,\n                        }\n                  }\n                >\n                  <span className=\"mr-1\">\n                    {stat.trend.direction === \"up\" ? \"↗\" : \"↘\"}\n                  </span>\n                  {stat.trend.value}\n                </motion.div>\n              ) : null}\n\n              {/* Hover effect background */}\n              <motion.div\n                className=\"absolute inset-0 bg-gradient-to-br from-brand/10 via-transparent to-transparent opacity-0 group-hover:opacity-100\"\n                initial={{ opacity: 0 }}\n                transition={{ duration: 0.3 }}\n                whileHover={{ opacity: 1 }}\n              />\n            </motion.div>\n          ))}\n        </div>\n      </div>\n    </section>\n  );\n}\n\nexport default StatsCards;\n","path":"index.tsx","target":"components/smoothui/stats-2/index.tsx","type":"registry:block"}],"name":"stats-2","registryDependencies":["https://smoothui.dev/r/tokens.json"],"title":"Stats 2","type":"registry:block"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"Grid team section block with hover effects","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { getAllPeople, getAvatarUrl, type Person } from \"@/lib/smoothui-data\";\nimport { motion, useInView, useReducedMotion } from \"motion/react\";\nimport { useRef } from \"react\";\n\nconst DEFAULT_MEMBER_COUNT = 4;\nconst AVATAR_SIZE = 400;\nconst STAGGER_DELAY = 0.1;\n\ninterface TeamGridProps {\n  description?: string;\n  members?: Person[];\n  title?: string;\n}\n\nexport function TeamGrid({\n  title = \"Our team\",\n  description = \"We're a dynamic group of individuals who are passionate about what we do and dedicated to delivering the best results for our clients.\",\n  members = getAllPeople().slice(0, DEFAULT_MEMBER_COUNT),\n}: TeamGridProps) {\n  const shouldReduceMotion = useReducedMotion();\n  const ref = useRef(null);\n  const isInView = useInView(ref, { once: true });\n\n  return (\n    <section className=\"bg-primary py-24 sm:py-32\">\n      <div className=\"mx-auto max-w-7xl px-6 lg:px-8\">\n        <motion.div\n          className=\"mx-auto max-w-2xl lg:mx-0\"\n          initial={shouldReduceMotion ? { opacity: 1 } : { opacity: 0, y: 20 }}\n          transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.6 }}\n          viewport={{ once: true }}\n          whileInView={\n            shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }\n          }\n        >\n          <h2 className=\"text-pretty font-semibold text-4xl text-foreground tracking-tight sm:text-5xl\">\n            {title}\n          </h2>\n          <p className=\"mt-6 text-foreground/70 text-lg/8\">{description}</p>\n        </motion.div>\n        <motion.ul\n          className=\"mx-auto mt-20 grid max-w-2xl grid-cols-1 gap-x-8 gap-y-14 sm:grid-cols-2 lg:mx-0 lg:max-w-none lg:grid-cols-3 xl:grid-cols-4\"\n          ref={ref}\n        >\n          {members.map((member, index) => (\n            <motion.li\n              animate={(() => {\n                if (shouldReduceMotion) {\n                  return { opacity: 1 };\n                }\n                return isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 30 };\n              })()}\n              initial={\n                shouldReduceMotion ? { opacity: 1 } : { opacity: 0, y: 30 }\n              }\n              key={member.name}\n              transition={\n                shouldReduceMotion\n                  ? { duration: 0 }\n                  : { delay: index * STAGGER_DELAY, duration: 0.6 }\n              }\n            >\n              <motion.div\n                className=\"group\"\n                transition={\n                  shouldReduceMotion\n                    ? { duration: 0 }\n                    : { damping: 20, stiffness: 300, type: \"spring\" as const }\n                }\n                whileHover={shouldReduceMotion ? {} : { scale: 1.02 }}\n              >\n                {/* Avatar */}\n                <motion.div\n                  className=\"relative overflow-hidden rounded-2xl\"\n                  transition={\n                    shouldReduceMotion\n                      ? { duration: 0 }\n                      : { damping: 20, stiffness: 300, type: \"spring\" as const }\n                  }\n                  whileHover={shouldReduceMotion ? {} : { scale: 1.05 }}\n                >\n                  <img\n                    alt={`Photo of ${member.name}`}\n                    className=\"aspect-14/13 w-full rounded-2xl object-cover outline-1 outline-black/5 -outline-offset-1 transition-all duration-300 group-hover:outline-black/10 dark:outline-white/10 dark:group-hover:outline-white/20\"\n                    draggable={false}\n                    height={AVATAR_SIZE}\n                    src={getAvatarUrl(member.avatar, AVATAR_SIZE)}\n                    width={AVATAR_SIZE}\n                  />\n                  <motion.div\n                    className=\"absolute inset-0 bg-gradient-to-br from-black/5 to-transparent opacity-0 group-hover:opacity-100\"\n                    initial={{ opacity: 0 }}\n                    transition={\n                      shouldReduceMotion ? { duration: 0 } : { duration: 0.3 }\n                    }\n                    whileHover={shouldReduceMotion ? {} : { opacity: 1 }}\n                  />\n                </motion.div>\n                {/* Name */}\n                <h3 className=\"mt-6 font-semibold text-foreground text-lg/8 tracking-tight\">\n                  {member.name}\n                </h3>\n                {/* Role */}\n                <p className=\"text-base/7 text-foreground/70\">{member.role}</p>\n                {/* Location */}\n                {member.location ? (\n                  <p className=\"text-foreground/70 text-sm/6\">\n                    {member.location}\n                  </p>\n                ) : null}\n                {/* Bio */}\n                {member.bio ? (\n                  <p className=\"mt-2 text-foreground/70 text-sm\">\n                    {member.bio}\n                  </p>\n                ) : null}\n              </motion.div>\n            </motion.li>\n          ))}\n        </motion.ul>\n      </div>\n    </section>\n  );\n}\n\nexport default TeamGrid;\n","path":"index.tsx","target":"components/smoothui/team-1/index.tsx","type":"registry:block"}],"name":"team-1","registryDependencies":["https://smoothui.dev/r/data.json"],"title":"Team 1","type":"registry:block"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"Carousel team section block with auto-play","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { getAllPeople, getAvatarUrl, type Person } from \"@/lib/smoothui-data\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { useEffect, useState } from \"react\";\n\nconst CARDS_PER_VIEW = 3; // Number of cards visible at once\nconst CARD_WIDTH = 288;\nconst CARD_GAP = 16;\nconst AUTOPLAY_INTERVAL = 5000;\nconst TRANSITION_TIMEOUT = 1500;\nconst AVATAR_SIZE = 160;\nconst STAGGER_DELAY = 0.1;\n\ninterface TeamCarouselProps {\n  description?: string;\n  members?: Person[];\n  subtitle?: string;\n  title?: string;\n}\n\nexport function TeamCarousel({\n  title = \"Tech Pioneers\",\n  subtitle = \"building the future\",\n  description = \"We bring together brilliant developers, engineers, and tech innovators to create groundbreaking digital solutions.\",\n  members = getAllPeople(),\n}: TeamCarouselProps) {\n  const shouldReduceMotion = useReducedMotion();\n  const [currentIndex, setCurrentIndex] = useState(0);\n  const [isAutoPlaying, setIsAutoPlaying] = useState(true);\n  const [isTransitioning, setIsTransitioning] = useState(false);\n\n  useEffect(() => {\n    if (!isAutoPlaying || isTransitioning) {\n      return;\n    }\n\n    const interval = setInterval(() => {\n      setCurrentIndex(\n        (prev) => (prev + 1) % (members.length - CARDS_PER_VIEW + 1)\n      );\n    }, AUTOPLAY_INTERVAL);\n\n    return () => clearInterval(interval);\n  }, [members.length, isAutoPlaying, isTransitioning]);\n\n  const nextSlide = () => {\n    if (isTransitioning) {\n      return;\n    }\n    const maxIndex = members.length - CARDS_PER_VIEW;\n    if (currentIndex >= maxIndex) {\n      return;\n    }\n\n    setIsTransitioning(true);\n    setCurrentIndex((prev) => Math.min(prev + 1, maxIndex));\n    setIsAutoPlaying(false);\n\n    setTimeout(() => {\n      setIsTransitioning(false);\n      setIsAutoPlaying(true);\n    }, TRANSITION_TIMEOUT);\n  };\n\n  const prevSlide = () => {\n    if (isTransitioning) {\n      return;\n    }\n    if (currentIndex <= 0) {\n      return;\n    }\n\n    setIsTransitioning(true);\n    setCurrentIndex((prev) => Math.max(prev - 1, 0));\n    setIsAutoPlaying(false);\n\n    setTimeout(() => {\n      setIsTransitioning(false);\n      setIsAutoPlaying(true);\n    }, TRANSITION_TIMEOUT);\n  };\n\n  return (\n    <section className=\"overflow-hidden py-32\">\n      <div className=\"mx-auto max-w-5xl px-8 lg:px-0\">\n        <motion.div\n          initial={shouldReduceMotion ? { opacity: 1 } : { opacity: 0, y: 20 }}\n          transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.6 }}\n          viewport={{ once: true }}\n          whileInView={\n            shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }\n          }\n        >\n          <h2 className=\"font-medium text-5xl md:text-6xl\">\n            {title} <br />\n            <span className=\"text-foreground/50\">{subtitle}</span>\n          </h2>\n          <p className=\"mt-6 max-w-md text-foreground/70\">{description}</p>\n        </motion.div>\n\n        <div className=\"relative\">\n          {/* Navigation Buttons */}\n          <div className=\"mt-4 hidden items-center justify-end gap-4 md:flex\">\n            <motion.button\n              className=\"static top-1/2 -left-12 inline-flex size-11 shrink-0 translate-x-0 translate-y-0 cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-full border bg-background font-medium text-sm shadow-xs outline-none transition-all hover:bg-accent hover:text-accent-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:border-input dark:bg-input/30 dark:aria-invalid:ring-destructive/40 dark:hover:bg-input/50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0\"\n              disabled={currentIndex === 0 || isTransitioning}\n              onClick={prevSlide}\n              type=\"button\"\n              whileHover={shouldReduceMotion ? {} : { scale: 1.05 }}\n              whileTap={shouldReduceMotion ? {} : { scale: 0.95 }}\n            >\n              <svg\n                aria-hidden=\"true\"\n                className=\"lucide lucide-arrow-left\"\n                fill=\"none\"\n                height=\"24\"\n                stroke=\"currentColor\"\n                strokeLinecap=\"round\"\n                strokeLinejoin=\"round\"\n                strokeWidth=\"2\"\n                viewBox=\"0 0 24 24\"\n                width=\"24\"\n                xmlns=\"http://www.w3.org/2000/svg\"\n              >\n                <path d=\"m12 19-7-7 7-7\" />\n                <path d=\"M19 12H5\" />\n              </svg>\n              <span className=\"sr-only\">Previous slide</span>\n            </motion.button>\n            <motion.button\n              className=\"static top-1/2 -right-12 inline-flex size-11 shrink-0 translate-x-0 translate-y-0 cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-full border bg-background font-medium text-sm shadow-xs outline-none transition-all hover:bg-accent hover:text-accent-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:border-input dark:bg-input/30 dark:aria-invalid:ring-destructive/40 dark:hover:bg-input/50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0\"\n              disabled={\n                currentIndex >= members.length - CARDS_PER_VIEW ||\n                isTransitioning\n              }\n              onClick={nextSlide}\n              type=\"button\"\n              whileHover={shouldReduceMotion ? {} : { scale: 1.05 }}\n              whileTap={shouldReduceMotion ? {} : { scale: 0.95 }}\n            >\n              <svg\n                aria-hidden=\"true\"\n                className=\"lucide lucide-arrow-right\"\n                fill=\"none\"\n                height=\"24\"\n                stroke=\"currentColor\"\n                strokeLinecap=\"round\"\n                strokeLinejoin=\"round\"\n                strokeWidth=\"2\"\n                viewBox=\"0 0 24 24\"\n                width=\"24\"\n                xmlns=\"http://www.w3.org/2000/svg\"\n              >\n                <path d=\"M5 12h14\" />\n                <path d=\"m12 5 7 7-7 7\" />\n              </svg>\n              <span className=\"sr-only\">Next slide</span>\n            </motion.button>\n          </div>\n\n          {/* Carousel Content */}\n          <div className=\"mt-16 [&>div[data-slot=carousel-content]]:overflow-visible\">\n            <div className=\"overflow-hidden\" data-slot=\"carousel-content\">\n              <motion.div\n                animate={{ x: `-${currentIndex * (CARD_WIDTH + CARD_GAP)}px` }}\n                className=\"-ml-4 flex max-w-[min(calc(100vw-4rem),24rem)] select-none\"\n                transition={\n                  shouldReduceMotion\n                    ? { duration: 0 }\n                    : { damping: 30, stiffness: 300, type: \"spring\" as const }\n                }\n              >\n                {members.map((member, index) => (\n                  <div\n                    className=\"min-w-0 max-w-72 shrink-0 grow-0 basis-full pl-4\"\n                    data-slot=\"carousel-item\"\n                    key={member.name}\n                  >\n                    <motion.div\n                      className=\"rounded-2xl border border-border bg-background p-7 text-center\"\n                      initial={\n                        shouldReduceMotion\n                          ? { opacity: 1 }\n                          : { opacity: 0, y: 20 }\n                      }\n                      transition={\n                        shouldReduceMotion\n                          ? { duration: 0 }\n                          : { delay: index * STAGGER_DELAY, duration: 0.5 }\n                      }\n                      viewport={{ once: true }}\n                      whileInView={\n                        shouldReduceMotion\n                          ? { opacity: 1 }\n                          : { opacity: 1, y: 0 }\n                      }\n                    >\n                      <img\n                        alt={member.name}\n                        className=\"mx-auto size-20 rounded-full border border-border\"\n                        draggable={false}\n                        height={80}\n                        src={getAvatarUrl(member.avatar, AVATAR_SIZE)}\n                        width={80}\n                      />\n                      <div className=\"mt-6 flex flex-col justify-center\">\n                        <p className=\"font-medium text-foreground text-lg\">\n                          {member.name}\n                        </p>\n                        <p className=\"text-muted-foreground text-sm\">\n                          {member.role}\n                        </p>\n                      </div>\n                      <div\n                        className=\"my-6 shrink-0 bg-border bg-linear-to-r from-background via-border to-background data-[orientation=horizontal]:h-px data-[orientation=vertical]:h-full data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px\"\n                        data-orientation=\"horizontal\"\n                        data-slot=\"separator-root\"\n                        role=\"none\"\n                      />\n                      <p className=\"text-muted-foreground text-sm\">\n                        {member.experience}\n                      </p>\n                    </motion.div>\n                  </div>\n                ))}\n              </motion.div>\n            </div>\n          </div>\n        </div>\n      </div>\n    </section>\n  );\n}\n\nexport default TeamCarousel;\n","path":"index.tsx","target":"components/smoothui/team-2/index.tsx","type":"registry:block"}],"name":"team-2","registryDependencies":["https://smoothui.dev/r/data.json"],"title":"Team 2","type":"registry:block"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"Simple testimonials block with auto-rotating cards","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { getAvatarUrl, getTestimonials } from \"@/lib/smoothui-data\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useEffect, useRef, useState } from \"react\";\n\nconst TESTIMONIAL_COUNT = 4;\nconst AVATAR_SIZE = 96;\nconst DURATION = 5000; // ms\nconst BAR_WIDTH = 50;\nconst CIRCLE_SIZE = 12;\nconst BORDER_RADIUS_ACTIVE = 8;\nconst BORDER_RADIUS_INACTIVE = 999;\nconst MILLISECONDS_TO_SECONDS = 1000;\n\nconst testimonials = getTestimonials(TESTIMONIAL_COUNT).map((testimonial) => ({\n  avatar: testimonial.avatar,\n  name: testimonial.name,\n  quote: testimonial.content || \"\",\n  role: testimonial.role,\n}));\n\nexport function TestimonialsSimple() {\n  const shouldReduceMotion = useReducedMotion();\n  const [index, setIndex] = useState(0);\n  const timeoutRef = useRef<NodeJS.Timeout | null>(null);\n\n  useEffect(() => {\n    timeoutRef.current = setTimeout(() => {\n      setIndex((prev) => (prev + 1) % testimonials.length);\n    }, DURATION);\n\n    return () => {\n      if (timeoutRef.current) {\n        clearTimeout(timeoutRef.current);\n      }\n    };\n  }, []);\n\n  return (\n    <section className=\"relative flex flex-col items-center bg-background py-16\">\n      <div className=\"flex w-full max-w-5xl flex-col items-center justify-center px-4\">\n        <div className=\"min-h-[120px] w-full\">\n          <AnimatePresence mode=\"wait\">\n            <motion.blockquote\n              animate={\n                shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }\n              }\n              className=\"mb-8 text-center font-semibold text-2xl text-foreground leading-tight md:text-4xl\"\n              exit={\n                shouldReduceMotion\n                  ? { opacity: 0, transition: { duration: 0 } }\n                  : { opacity: 0, y: -30 }\n              }\n              initial={\n                shouldReduceMotion ? { opacity: 1 } : { opacity: 0, y: 30 }\n              }\n              key={index}\n              transition={\n                shouldReduceMotion\n                  ? { duration: 0 }\n                  : { duration: 0.5, type: \"spring\" as const }\n              }\n            >\n              &ldquo;{testimonials[index].quote}&rdquo;\n            </motion.blockquote>\n          </AnimatePresence>\n        </div>\n        <div className=\"flex w-full max-w-lg items-center justify-center gap-8 pt-8\">\n          <AnimatePresence initial={false} mode=\"wait\">\n            <motion.div\n              animate={\n                shouldReduceMotion\n                  ? { opacity: 1 }\n                  : { filter: \"blur(0px)\", opacity: 1 }\n              }\n              className=\"flex items-center gap-4\"\n              exit={\n                shouldReduceMotion\n                  ? { opacity: 0, transition: { duration: 0 } }\n                  : { filter: \"blur(8px)\", opacity: 0 }\n              }\n              initial={\n                shouldReduceMotion\n                  ? { opacity: 1 }\n                  : { filter: \"blur(8px)\", opacity: 0 }\n              }\n              key={index}\n              transition={\n                shouldReduceMotion\n                  ? { duration: 0 }\n                  : { duration: 0.5, type: \"spring\" as const }\n              }\n            >\n              <img\n                alt={`${testimonials[index].name} avatar`}\n                className=\"h-12 w-12 rounded-full border bg-foreground/10 object-cover\"\n                draggable={false}\n                height={48}\n                src={getAvatarUrl(testimonials[index].avatar, AVATAR_SIZE)}\n                width={48}\n              />\n              <div className=\"mx-4 h-8 border-muted-foreground/30 border-l\" />\n              <div className=\"text-left\">\n                <div className=\"font-medium text-foreground text-lg italic\">\n                  {testimonials[index].name}\n                </div>\n                <div className=\"text-base text-muted-foreground\">\n                  {testimonials[index].role}\n                </div>\n              </div>\n            </motion.div>\n          </AnimatePresence>\n        </div>\n        {/* Progress Bar & Circles Indicator */}\n        <div className=\"mx-auto mt-8 flex w-full max-w-lg justify-center gap-3\">\n          {testimonials.map((testimonial, i) => {\n            const isActive = i === index;\n            return (\n              <motion.span\n                animate={\n                  shouldReduceMotion\n                    ? {\n                        borderRadius: isActive\n                          ? BORDER_RADIUS_ACTIVE\n                          : BORDER_RADIUS_INACTIVE,\n                        height: CIRCLE_SIZE,\n                        width: isActive ? BAR_WIDTH : CIRCLE_SIZE,\n                      }\n                    : {\n                        borderRadius: isActive\n                          ? BORDER_RADIUS_ACTIVE\n                          : BORDER_RADIUS_INACTIVE,\n                        height: CIRCLE_SIZE,\n                        width: isActive ? BAR_WIDTH : CIRCLE_SIZE,\n                      }\n                }\n                className=\"relative block overflow-hidden bg-foreground/10\"\n                initial={false}\n                key={`testimonial-${testimonial.name}-${i}`}\n                layout={!shouldReduceMotion}\n                style={{\n                  border: \"none\",\n                  maxWidth: BAR_WIDTH,\n                  minWidth: CIRCLE_SIZE,\n                }}\n                transition={\n                  shouldReduceMotion\n                    ? { duration: 0 }\n                    : {\n                        damping: 30,\n                        duration: 0.4,\n                        stiffness: 300,\n                        type: \"spring\" as const,\n                      }\n                }\n              >\n                {isActive && (\n                  <motion.div\n                    animate={{ width: \"100%\" }}\n                    className=\"absolute top-0 left-0 h-full rounded-lg bg-brand\"\n                    exit={{ width: 0 }}\n                    initial={\n                      shouldReduceMotion ? { width: \"100%\" } : { width: 0 }\n                    }\n                    key={index}\n                    transition={\n                      shouldReduceMotion\n                        ? { duration: 0 }\n                        : {\n                            duration: DURATION / MILLISECONDS_TO_SECONDS,\n                            ease: \"linear\",\n                          }\n                    }\n                  />\n                )}\n              </motion.span>\n            );\n          })}\n        </div>\n      </div>\n    </section>\n  );\n}\n\nexport default TestimonialsSimple;\n","path":"index.tsx","target":"components/smoothui/testimonials-1/index.tsx","type":"registry:block"}],"name":"testimonials-1","registryDependencies":["https://smoothui.dev/r/data.json","https://smoothui.dev/r/tokens.json"],"title":"Testimonials 1","type":"registry:block"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["lucide-react","motion"],"description":"Grid testimonials block with navigation arrows","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport {\n  Avatar,\n  AvatarFallback,\n  AvatarImage,\n} from \"@/components/ui/avatar\";\nimport { getAvatarUrl, getTestimonials } from \"@/lib/smoothui-data\";\nimport { ChevronLeft, ChevronRight } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useCallback, useEffect, useState } from \"react\";\n\nconst testimonials = getTestimonials(7);\n\nexport function TestimonialsGrid() {\n  const shouldReduceMotion = useReducedMotion();\n  const [active, setActive] = useState(0);\n  const [autoplay] = useState(false);\n\n  const handleNext = useCallback(() => {\n    setActive((prev) => (prev + 1) % testimonials.length);\n  }, []);\n\n  const handlePrev = () => {\n    setActive((prev) => (prev - 1 + testimonials.length) % testimonials.length);\n  };\n\n  const isActive = (index: number) => index === active;\n\n  useEffect(() => {\n    if (autoplay) {\n      const interval = setInterval(handleNext, 5000);\n      return () => clearInterval(interval);\n    }\n  }, [autoplay, handleNext]);\n\n  return (\n    <section>\n      <div className=\"min-h-auto bg-muted py-24\">\n        <div className=\"container mx-auto w-full max-w-6xl px-6\">\n          <motion.div\n            animate={shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }}\n            className=\"mb-12\"\n            initial={\n              shouldReduceMotion ? { opacity: 1 } : { opacity: 0, y: 20 }\n            }\n            transition={\n              shouldReduceMotion\n                ? { duration: 0 }\n                : { duration: 0.6, ease: [0.22, 1, 0.36, 1] }\n            }\n          >\n            {/* Layout: Title on left, Testimonial on right */}\n            <div className=\"grid grid-cols-1 gap-8 lg:grid-cols-2 lg:gap-12\">\n              {/* Left side - Title and description */}\n              <div className=\"flex flex-col justify-center\">\n                <h2 className=\"mb-4 font-semibold text-4xl text-foreground\">\n                  What Developers Say\n                </h2>\n                <p className=\"text-balance text-foreground/70 text-lg\">\n                  Join thousands of developers who are building faster, more\n                  beautiful UIs with SmoothUI. See what they&apos;re saying\n                  about their experience.\n                </p>\n              </div>\n              {/* Right side - Testimonial card */}\n              <div className=\"relative flex min-h-fit flex-col items-end\">\n                {/* Navigation Arrows - Above the card */}\n                <div className=\"mb-4 flex justify-center gap-2\">\n                  <motion.button\n                    animate={\n                      shouldReduceMotion ? { opacity: 1 } : { opacity: 1, x: 0 }\n                    }\n                    aria-label=\"Previous testimonial\"\n                    className=\"group/button flex h-8 w-8 cursor-pointer items-center justify-center rounded-full border bg-background shadow-lg transition-all duration-200 hover:scale-110 hover:shadow-xl\"\n                    initial={\n                      shouldReduceMotion\n                        ? { opacity: 1 }\n                        : { opacity: 0, x: -20 }\n                    }\n                    onClick={handlePrev}\n                    transition={\n                      shouldReduceMotion\n                        ? { duration: 0 }\n                        : { delay: 0.4, duration: 0.3 }\n                    }\n                    type=\"button\"\n                    whileHover={shouldReduceMotion ? {} : { scale: 1.1 }}\n                    whileTap={shouldReduceMotion ? {} : { scale: 0.95 }}\n                  >\n                    <ChevronLeft className=\"h-5 w-5 text-foreground transition-transform duration-300 group-hover/button:-rotate-12\" />\n                  </motion.button>\n                  <motion.button\n                    animate={\n                      shouldReduceMotion ? { opacity: 1 } : { opacity: 1, x: 0 }\n                    }\n                    aria-label=\"Next testimonial\"\n                    className=\"group/button flex h-8 w-8 cursor-pointer items-center justify-center rounded-full border bg-background shadow-lg transition-all duration-200 hover:scale-110 hover:shadow-xl\"\n                    initial={\n                      shouldReduceMotion\n                        ? { opacity: 1 }\n                        : { opacity: 0, x: 20 }\n                    }\n                    onClick={handleNext}\n                    transition={\n                      shouldReduceMotion\n                        ? { duration: 0 }\n                        : { delay: 0.4, duration: 0.3 }\n                    }\n                    type=\"button\"\n                    whileHover={shouldReduceMotion ? {} : { scale: 1.1 }}\n                    whileTap={shouldReduceMotion ? {} : { scale: 0.95 }}\n                  >\n                    <ChevronRight className=\"h-5 w-5 text-foreground transition-transform duration-300 group-hover/button:rotate-12\" />\n                  </motion.button>\n                </div>\n                <div className=\"relative h-full w-full max-w-md\">\n                  <AnimatePresence>\n                    {testimonials.map((testimonial, index) => (\n                      <motion.div\n                        animate={\n                          shouldReduceMotion\n                            ? { opacity: isActive(index) ? 1 : 0 }\n                            : {\n                                opacity: isActive(index) ? 1 : 0,\n                                scale: isActive(index) ? 1 : 0.95,\n                                y: isActive(index) ? 0 : 30,\n                              }\n                        }\n                        className={`absolute inset-0 min-h-fit ${isActive(index) ? \"z-10\" : \"z-0\"}`}\n                        exit={\n                          shouldReduceMotion\n                            ? { opacity: 0, transition: { duration: 0 } }\n                            : {\n                                opacity: 0,\n                                scale: 0.9,\n                                y: -30,\n                              }\n                        }\n                        initial={\n                          shouldReduceMotion\n                            ? { opacity: 0 }\n                            : {\n                                opacity: 0,\n                                scale: 0.9,\n                                y: 30,\n                              }\n                        }\n                        key={testimonial.name}\n                        transition={\n                          shouldReduceMotion\n                            ? { duration: 0 }\n                            : {\n                                duration: 0.4,\n                                ease: \"easeInOut\",\n                              }\n                        }\n                      >\n                        <div className=\"rounded-2xl border bg-background px-6 py-6 shadow-lg transition-all duration-200\">\n                          <motion.p\n                            animate={\n                              shouldReduceMotion\n                                ? { opacity: 1 }\n                                : { opacity: 1, y: 0 }\n                            }\n                            className=\"mb-6 text-foreground text-lg\"\n                            initial={\n                              shouldReduceMotion\n                                ? { opacity: 1 }\n                                : { opacity: 0, y: 10 }\n                            }\n                            key={active}\n                            transition={\n                              shouldReduceMotion\n                                ? { duration: 0 }\n                                : { duration: 0.3 }\n                            }\n                          >\n                            {(testimonial.content || \"\")\n                              .split(\" \")\n                              .map((word, wordIndex) => (\n                                <motion.span\n                                  animate={\n                                    shouldReduceMotion\n                                      ? { opacity: 1 }\n                                      : {\n                                          filter: \"blur(0px)\",\n                                          opacity: 1,\n                                          y: 0,\n                                        }\n                                  }\n                                  className=\"inline-block\"\n                                  initial={\n                                    shouldReduceMotion\n                                      ? { opacity: 1 }\n                                      : {\n                                          filter: \"blur(4px)\",\n                                          opacity: 0,\n                                          y: 5,\n                                        }\n                                  }\n                                  key={`${testimonial.name}-word-${wordIndex}`}\n                                  transition={\n                                    shouldReduceMotion\n                                      ? { duration: 0 }\n                                      : {\n                                          delay: wordIndex * 0.02,\n                                          duration: 0.2,\n                                          ease: \"easeInOut\",\n                                        }\n                                  }\n                                >\n                                  {word}&nbsp;\n                                </motion.span>\n                              ))}\n                          </motion.p>\n                          <motion.div\n                            animate={\n                              shouldReduceMotion\n                                ? { opacity: 1 }\n                                : { opacity: 1, y: 0 }\n                            }\n                            className=\"flex items-center gap-3\"\n                            initial={\n                              shouldReduceMotion\n                                ? { opacity: 1 }\n                                : { opacity: 0, y: 10 }\n                            }\n                            transition={\n                              shouldReduceMotion\n                                ? { duration: 0 }\n                                : { delay: 0.2, duration: 0.3 }\n                            }\n                          >\n                            <Avatar className=\"size-8 border border-transparent shadow ring-1 ring-foreground/10\">\n                              <AvatarImage\n                                alt={testimonial.name}\n                                src={getAvatarUrl(testimonial.avatar, 64)}\n                              />\n                              <AvatarFallback>\n                                {testimonial.name.charAt(0)}\n                              </AvatarFallback>\n                            </Avatar>\n                            <div>\n                              <div className=\"font-semibold text-foreground\">\n                                {testimonial.name}\n                              </div>\n                              <span className=\"text-muted-foreground text-sm\">\n                                {testimonial.role}\n                              </span>\n                            </div>\n                          </motion.div>\n                        </div>\n                      </motion.div>\n                    ))}\n                  </AnimatePresence>\n                </div>\n              </div>\n            </div>\n          </motion.div>\n        </div>\n      </div>\n    </section>\n  );\n}\n\nexport default TestimonialsGrid;\n","path":"index.tsx","target":"components/smoothui/testimonials-2/index.tsx","type":"registry:block"}],"name":"testimonials-2","registryDependencies":["avatar","https://smoothui.dev/r/data.json"],"title":"Testimonials 2","type":"registry:block"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["lucide-react","motion"],"description":"Star testimonials block with grid layout","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport {\n  Avatar,\n  AvatarFallback,\n  AvatarImage,\n} from \"@/components/ui/avatar\";\nimport { cn } from \"@/lib/utils\";\nimport { getAvatarUrl, getTestimonials } from \"@/lib/smoothui-data\";\nimport { Star } from \"lucide-react\";\nimport { motion, useReducedMotion } from \"motion/react\";\n\nconst testimonials = getTestimonials(4);\n\nexport function TestimonialsStars() {\n  const shouldReduceMotion = useReducedMotion();\n  return (\n    <section>\n      <div className=\"py-24\">\n        <div className=\"container mx-auto w-full max-w-5xl px-6\">\n          <motion.div\n            animate={shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }}\n            className=\"mb-12\"\n            initial={\n              shouldReduceMotion ? { opacity: 1 } : { opacity: 0, y: 20 }\n            }\n            transition={\n              shouldReduceMotion\n                ? { duration: 0 }\n                : { duration: 0.6, ease: [0.22, 1, 0.36, 1] }\n            }\n          >\n            <h2 className=\"font-semibold text-4xl text-foreground\">\n              Developer Reviews\n            </h2>\n            <p className=\"my-4 text-balance text-lg text-muted-foreground\">\n              See what the community is saying about SmoothUI. Real feedback\n              from developers building amazing user experiences.\n            </p>\n          </motion.div>\n\n          <motion.div\n            animate={shouldReduceMotion ? { opacity: 1 } : { opacity: 1 }}\n            className=\"grid 3xl:grid-cols-3 3xl:gap-12 gap-6 lg:grid-cols-2\"\n            initial={shouldReduceMotion ? { opacity: 1 } : { opacity: 0 }}\n            transition={\n              shouldReduceMotion\n                ? { duration: 0 }\n                : { delay: 0.2, duration: 0.6, ease: [0.22, 1, 0.36, 1] }\n            }\n          >\n            {testimonials.map((testimonial, index) => (\n              <motion.div\n                animate={\n                  shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }\n                }\n                className=\"group rounded-2xl border border-transparent px-4 py-3 duration-200 hover:border-border hover:bg-background/50\"\n                initial={\n                  shouldReduceMotion ? { opacity: 1 } : { opacity: 0, y: 30 }\n                }\n                key={testimonial.name}\n                transition={\n                  shouldReduceMotion\n                    ? { duration: 0 }\n                    : {\n                        delay: index * 0.15,\n                        duration: 0.5,\n                        ease: [0.22, 1, 0.36, 1],\n                      }\n                }\n                whileHover={\n                  shouldReduceMotion\n                    ? {}\n                    : {\n                        transition: { duration: 0.2, ease: [0.22, 1, 0.36, 1] },\n                        y: -4,\n                      }\n                }\n              >\n                <motion.div\n                  animate={\n                    shouldReduceMotion\n                      ? { opacity: 1 }\n                      : { opacity: 1, scale: 1 }\n                  }\n                  className=\"flex gap-1\"\n                  initial={\n                    shouldReduceMotion\n                      ? { opacity: 1 }\n                      : { opacity: 0, scale: 0.8 }\n                  }\n                  transition={\n                    shouldReduceMotion\n                      ? { duration: 0 }\n                      : {\n                          delay: index * 0.15 + 0.2,\n                          duration: 0.4,\n                          ease: [0.22, 1, 0.36, 1],\n                        }\n                  }\n                >\n                  {Array.from({ length: 5 }).map((_, i) => (\n                    <motion.div\n                      animate={\n                        shouldReduceMotion\n                          ? { opacity: 1 }\n                          : { opacity: 1, scale: 1 }\n                      }\n                      initial={\n                        shouldReduceMotion\n                          ? { opacity: 1 }\n                          : { opacity: 0, scale: 0 }\n                      }\n                      key={`${testimonial.name}-star-${i}`}\n                      transition={\n                        shouldReduceMotion\n                          ? { duration: 0 }\n                          : {\n                              delay: index * 0.15 + 0.2 + i * 0.05,\n                              duration: 0.3,\n                              ease: [0.68, -0.55, 0.265, 1.55],\n                            }\n                      }\n                    >\n                      <Star\n                        className={cn(\n                          \"size-4 transition-colors duration-200\",\n                          i < (testimonial.stars || 0)\n                            ? \"fill-accent stroke-accent\"\n                            : \"fill-primary stroke-border\"\n                        )}\n                      />\n                    </motion.div>\n                  ))}\n                </motion.div>\n\n                <motion.p\n                  animate={\n                    shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }\n                  }\n                  className=\"my-4 text-foreground\"\n                  initial={\n                    shouldReduceMotion ? { opacity: 1 } : { opacity: 0, y: 10 }\n                  }\n                  transition={\n                    shouldReduceMotion\n                      ? { duration: 0 }\n                      : {\n                          delay: index * 0.15 + 0.4,\n                          duration: 0.4,\n                          ease: [0.22, 1, 0.36, 1],\n                        }\n                  }\n                >\n                  {testimonial.content}\n                </motion.p>\n\n                <motion.div\n                  animate={\n                    shouldReduceMotion ? { opacity: 1 } : { opacity: 1, x: 0 }\n                  }\n                  className=\"flex items-center gap-2\"\n                  initial={\n                    shouldReduceMotion ? { opacity: 1 } : { opacity: 0, x: -10 }\n                  }\n                  transition={\n                    shouldReduceMotion\n                      ? { duration: 0 }\n                      : {\n                          delay: index * 0.15 + 0.5,\n                          duration: 0.3,\n                          ease: [0.22, 1, 0.36, 1],\n                        }\n                  }\n                >\n                  <Avatar className=\"size-6 border border-transparent shadow ring-1 ring-foreground/10\">\n                    <AvatarImage\n                      alt={testimonial.name}\n                      src={getAvatarUrl(testimonial.avatar, 48)}\n                    />\n                    <AvatarFallback>\n                      {testimonial.name.charAt(0)}\n                    </AvatarFallback>\n                  </Avatar>\n                  <div className=\"font-medium text-foreground text-sm\">\n                    {testimonial.name}\n                  </div>\n                  <span\n                    aria-hidden=\"true\"\n                    className=\"size-1 rounded-full bg-foreground/25\"\n                  />\n                  <span className=\"text-muted-foreground text-sm\">\n                    {testimonial.role}\n                  </span>\n                </motion.div>\n              </motion.div>\n            ))}\n          </motion.div>\n        </div>\n      </div>\n    </section>\n  );\n}\n\nexport default TestimonialsStars;\n","path":"index.tsx","target":"components/smoothui/testimonials-3/index.tsx","type":"registry:block"}],"name":"testimonials-3","registryDependencies":["avatar","https://smoothui.dev/r/data.json"],"title":"Testimonials 3","type":"registry:block"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":[],"description":"Canvas-based generative pixel avatar for AI agents, unique per seed.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { useEffect, useRef } from \"react\";\n\nexport type AgentAvatarProps = Omit<\n  React.CanvasHTMLAttributes<HTMLCanvasElement>,\n  \"children\"\n> & {\n  /** String seed to generate a unique deterministic avatar pattern */\n  seed: string;\n  /** Diameter in pixels */\n  size?: number;\n  /** Enable pixel animation (respects prefers-reduced-motion) */\n  animated?: boolean;\n};\n\nconst GRID_SIZE = 6;\n\n/** Pulse: each pixel oscillates lightness independently */\nconst PULSE_SPEED = 0.002;\nconst PULSE_AMPLITUDE = 22;\n\n/** Breathe: global slow scale oscillation */\nconst BREATHE_SPEED = 0.001;\nconst BREATHE_AMPLITUDE = 10;\n\n/** Wave: diagonal sweep across the grid */\nconst WAVE_SPEED = 0.0015;\nconst WAVE_AMPLITUDE = 15;\nconst WAVE_LENGTH = 3;\n\n/** Sparkle: random bright flashes */\nconst SPARKLE_SPEED = 0.004;\nconst SPARKLE_THRESHOLD = 0.92;\nconst SPARKLE_BOOST = 25;\n\n/** Scale pulse: whole avatar breathes in size */\nconst SCALE_PULSE_SPEED = 0.0008;\nconst SCALE_PULSE_AMOUNT = 0.03;\n\n/** Max hue spread from base — wider for richer color variation */\nconst HUE_SPREAD = 45;\n\nconst GLOW_RADIUS_RATIO = 0.25;\n\n/** Simple deterministic hash from a string */\nconst hashSeed = (str: string): number => {\n  let hash = 0;\n  for (const char of str) {\n    hash = ((hash << 5) - hash + char.charCodeAt(0)) | 0;\n  }\n  return Math.abs(hash);\n};\n\n/** Seeded PRNG (mulberry32) */\nconst createRng = (seed: number) => {\n  let state = seed;\n  return () => {\n    state = (state + 0x6d_2b_79_f5) | 0;\n    let t = Math.imul(state ^ (state >>> 15), 1 | state);\n    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;\n    return ((t ^ (t >>> 14)) >>> 0) / 4_294_967_296;\n  };\n};\n\ntype HSL = [hue: number, saturation: number, lightness: number];\n\n/** Derive a 3-color palette within the same hue family */\nconst generatePalette = (hash: number): [HSL, HSL, HSL] => {\n  const rng = createRng(hash);\n  const baseHue = rng() * 360;\n  const sat = 75 + rng() * 20; // 75-95%\n\n  return [\n    [baseHue, sat, 55 + rng() * 10],\n    [\n      (baseHue - HUE_SPREAD + rng() * HUE_SPREAD * 2) % 360,\n      sat - 5 + rng() * 10,\n      40 + rng() * 15,\n    ],\n    [\n      (baseHue - HUE_SPREAD + rng() * HUE_SPREAD * 2) % 360,\n      sat - 10 + rng() * 15,\n      60 + rng() * 15,\n    ],\n  ];\n};\n\ntype Cell = {\n  colorIndex: number;\n  phase: number;\n  brightness: number;\n  sparklePhase: number;\n};\n\n/** Build a grid with per-cell metadata */\nconst generateGrid = (hash: number): Cell[][] => {\n  const rng = createRng(hash + 1);\n  const grid: Cell[][] = [];\n\n  for (let y = 0; y < GRID_SIZE; y++) {\n    grid[y] = [];\n    for (let x = 0; x < GRID_SIZE; x++) {\n      grid[y][x] = {\n        brightness: 0.3 + rng() * 0.7,\n        colorIndex: Math.floor(rng() * 3),\n        phase: rng() * Math.PI * 2,\n        sparklePhase: rng() * Math.PI * 2,\n      };\n    }\n  }\n\n  return grid;\n};\n\nconst AgentAvatar = ({\n  seed,\n  size = 64,\n  animated = true,\n  className,\n  ...props\n}: AgentAvatarProps) => {\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const rafRef = useRef<number>(0);\n\n  useEffect(() => {\n    const canvas = canvasRef.current;\n    if (!canvas) {\n      return;\n    }\n\n    const ctx = canvas.getContext(\"2d\");\n    if (!ctx) {\n      return;\n    }\n\n    const dpr = window.devicePixelRatio || 1;\n    canvas.width = size * dpr;\n    canvas.height = size * dpr;\n    ctx.scale(dpr, dpr);\n\n    const hash = hashSeed(seed);\n    const palette = generatePalette(hash);\n    const grid = generateGrid(hash);\n    const cellSize = size / GRID_SIZE;\n    const half = size / 2;\n\n    const motionQuery = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    let shouldAnimate = animated && !motionQuery.matches;\n\n    const draw = (time: number) => {\n      ctx.clearRect(0, 0, size, size);\n\n      // Scale pulse — whole avatar breathes\n      const scale = shouldAnimate\n        ? 1 + Math.sin(time * SCALE_PULSE_SPEED) * SCALE_PULSE_AMOUNT\n        : 1;\n\n      ctx.save();\n      ctx.translate(half, half);\n      ctx.scale(scale, scale);\n      ctx.translate(-half, -half);\n\n      // Clip to circle\n      ctx.beginPath();\n      ctx.arc(half, half, half, 0, Math.PI * 2);\n      ctx.clip();\n\n      // Dark background\n      ctx.fillStyle = \"#08080f\";\n      ctx.fillRect(0, 0, size, size);\n\n      // Global breathe offset for lightness\n      const breatheOffset = shouldAnimate\n        ? Math.sin(time * BREATHE_SPEED) * BREATHE_AMPLITUDE\n        : 0;\n\n      // Draw pixel grid\n      for (let y = 0; y < GRID_SIZE; y++) {\n        for (let x = 0; x < GRID_SIZE; x++) {\n          const cell = grid[y][x];\n          const [h, s, l] = palette[cell.colorIndex];\n\n          // Per-pixel pulse\n          const pulse = shouldAnimate\n            ? Math.sin(time * PULSE_SPEED + cell.phase) * PULSE_AMPLITUDE\n            : 0;\n\n          // Diagonal wave sweep\n          const waveDist = (x + y) / WAVE_LENGTH;\n          const wave = shouldAnimate\n            ? Math.sin(time * WAVE_SPEED + waveDist) * WAVE_AMPLITUDE\n            : 0;\n\n          // Sparkle — occasional bright flash\n          const sparkleVal = shouldAnimate\n            ? Math.sin(time * SPARKLE_SPEED + cell.sparklePhase)\n            : 0;\n          const sparkle =\n            sparkleVal > SPARKLE_THRESHOLD\n              ? ((sparkleVal - SPARKLE_THRESHOLD) / (1 - SPARKLE_THRESHOLD)) *\n                SPARKLE_BOOST\n              : 0;\n\n          const finalLight = Math.min(\n            90,\n            Math.max(\n              20,\n              (l + pulse + breatheOffset + wave + sparkle) * cell.brightness\n            )\n          );\n          const finalSat = Math.min(100, s + 5);\n\n          // Pixel glow — subtle shadow per cell\n          ctx.shadowColor = `hsl(${h}, ${finalSat}%, ${finalLight}%)`;\n          ctx.shadowBlur = cellSize * 0.45;\n\n          ctx.fillStyle = `hsl(${h}, ${finalSat}%, ${finalLight}%)`;\n          ctx.fillRect(x * cellSize, y * cellSize, cellSize, cellSize);\n        }\n      }\n\n      // Reset shadow before restore\n      ctx.shadowBlur = 0;\n      ctx.restore();\n\n      // Outer glow ring\n      const [gh, gs, gl] = palette[0];\n      ctx.save();\n      ctx.globalCompositeOperation = \"screen\";\n      ctx.shadowColor = `hsla(${gh}, ${gs}%, ${gl}%, 0.6)`;\n      ctx.shadowBlur = size * GLOW_RADIUS_RATIO;\n      ctx.beginPath();\n      ctx.arc(half, half, half - 1, 0, Math.PI * 2);\n      ctx.strokeStyle = `hsla(${gh}, ${gs}%, ${gl}%, 0.15)`;\n      ctx.lineWidth = 2;\n      ctx.stroke();\n      ctx.restore();\n\n      if (shouldAnimate) {\n        rafRef.current = requestAnimationFrame(draw);\n      }\n    };\n\n    const handleMotionChange = () => {\n      cancelAnimationFrame(rafRef.current);\n      shouldAnimate = animated && !motionQuery.matches;\n      if (shouldAnimate) {\n        rafRef.current = requestAnimationFrame(draw);\n      } else {\n        draw(0);\n      }\n    };\n\n    motionQuery.addEventListener(\"change\", handleMotionChange);\n\n    if (shouldAnimate) {\n      rafRef.current = requestAnimationFrame(draw);\n    } else {\n      draw(0);\n    }\n\n    return () => {\n      cancelAnimationFrame(rafRef.current);\n      motionQuery.removeEventListener(\"change\", handleMotionChange);\n    };\n  }, [seed, size, animated]);\n\n  return (\n    <canvas\n      aria-label={`Avatar for ${seed}`}\n      className={cn(\"rounded-full\", className)}\n      ref={canvasRef}\n      role=\"img\"\n      style={{ height: size, width: size }}\n      {...props}\n    />\n  );\n};\n\nexport default AgentAvatar;\n","path":"index.tsx","target":"components/smoothui/agent-avatar/index.tsx","type":"registry:ui"}],"name":"agent-avatar","registryDependencies":[],"title":"Agent Avatar","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["lucide-react","motion"],"description":"Human-in-the-loop approval card where the chosen option expands to fill the card while the alternatives collapse away.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { Check } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { type ReactNode, useState } from \"react\";\n\nconst SPRING_DEFAULT = {\n  bounce: 0.1,\n  duration: 0.25,\n  type: \"spring\" as const,\n};\nconst EASE_OUT = [0.23, 1, 0.32, 1] as const;\nconst CHOICE_STAGGER = 0.03;\n\nexport type AIApprovalOption = {\n  /** Marks the option as the destructive one, e.g. \"Delete everything\". */\n  destructive?: boolean;\n  /** Secondary line under the label. */\n  detail?: string;\n  id: string;\n  label: string;\n};\n\nexport type AIApprovalProps = {\n  className?: string;\n  /** Extra context under the question. */\n  children?: ReactNode;\n  /** Called once, with the chosen option. */\n  onDecide?: (option: AIApprovalOption) => void;\n  options: AIApprovalOption[];\n  /** What the agent needs a human to settle. */\n  question: string;\n  /** Render already-resolved, e.g. when replaying a transcript. */\n  resolvedId?: string;\n};\n\n/**\n * The card an agent puts up before it acts.\n *\n * Choosing does not tick a radio button — the chosen option expands to fill the\n * card and the alternatives collapse out of existence. The decision should look\n * as irreversible as it is; leaving the rejected options sitting there greyed out\n * invites a second look at something that already happened.\n */\nconst AIApproval = ({\n  className,\n  children,\n  onDecide,\n  options,\n  question,\n  resolvedId,\n}: AIApprovalProps) => {\n  const shouldReduceMotion = useReducedMotion();\n  const [chosenId, setChosenId] = useState<string | null>(resolvedId ?? null);\n\n  const chosen = options.find((option) => option.id === chosenId) ?? null;\n\n  const decide = (option: AIApprovalOption) => {\n    if (chosenId) {\n      return;\n    }\n    setChosenId(option.id);\n    onDecide?.(option);\n  };\n\n  return (\n    <motion.div\n      className={cn(\n        \"w-full rounded-xl border border-border bg-background p-3.5\",\n        className\n      )}\n      layout={!shouldReduceMotion}\n      transition={shouldReduceMotion ? { duration: 0 } : SPRING_DEFAULT}\n    >\n      <motion.div layout={!shouldReduceMotion}>\n        <p className=\"font-medium text-foreground text-sm\">{question}</p>\n        {children ? (\n          <div className=\"mt-1 text-muted-foreground text-xs leading-relaxed\">\n            {children}\n          </div>\n        ) : null}\n      </motion.div>\n\n      <div className=\"mt-3\">\n        <AnimatePresence initial={false} mode=\"popLayout\">\n          {chosen ? (\n            <motion.div\n              animate={{ opacity: 1, scale: 1 }}\n              className={cn(\n                \"flex items-center gap-2 rounded-lg px-3 py-2 text-sm\",\n                chosen.destructive\n                  ? \"bg-destructive/10 text-destructive\"\n                  : \"bg-foreground text-background\"\n              )}\n              initial={\n                shouldReduceMotion\n                  ? { opacity: 1, scale: 1 }\n                  : { opacity: 0, scale: 0.97 }\n              }\n              key=\"resolved\"\n              transition={shouldReduceMotion ? { duration: 0 } : SPRING_DEFAULT}\n            >\n              <span className=\"flex size-4 items-center justify-center\">\n                <Check aria-hidden=\"true\" size={14} />\n              </span>\n              <span className=\"font-medium\">{chosen.label}</span>\n              {chosen.detail ? (\n                <span className=\"ml-auto text-xs opacity-70\">\n                  {chosen.detail}\n                </span>\n              ) : null}\n            </motion.div>\n          ) : (\n            <motion.div\n              className=\"flex flex-col gap-1.5\"\n              exit={\n                shouldReduceMotion\n                  ? { opacity: 0, transition: { duration: 0 } }\n                  : { opacity: 0, scale: 0.98 }\n              }\n              key=\"options\"\n              transition={\n                shouldReduceMotion\n                  ? { duration: 0 }\n                  : { duration: 0.16, ease: EASE_OUT }\n              }\n            >\n              {options.map((option, index) => (\n                <motion.button\n                  animate={{ opacity: 1, y: 0 }}\n                  className={cn(\n                    \"flex w-full cursor-pointer items-center gap-2 rounded-lg border px-3 py-2 text-left text-sm transition-colors\",\n                    option.destructive\n                      ? \"border-destructive/30 text-destructive hover:bg-destructive/10\"\n                      : \"border-border text-foreground hover:bg-muted\"\n                  )}\n                  initial={\n                    shouldReduceMotion\n                      ? { opacity: 1, y: 0 }\n                      : { opacity: 0, y: 4 }\n                  }\n                  key={option.id}\n                  onClick={() => decide(option)}\n                  transition={\n                    shouldReduceMotion\n                      ? { duration: 0 }\n                      : { ...SPRING_DEFAULT, delay: index * CHOICE_STAGGER }\n                  }\n                  type=\"button\"\n                  whileTap={shouldReduceMotion ? undefined : { scale: 0.99 }}\n                >\n                  <span className=\"font-medium\">{option.label}</span>\n                  {option.detail ? (\n                    <span className=\"ml-auto text-muted-foreground text-xs\">\n                      {option.detail}\n                    </span>\n                  ) : null}\n                </motion.button>\n              ))}\n            </motion.div>\n          )}\n        </AnimatePresence>\n      </div>\n    </motion.div>\n  );\n};\n\nexport default AIApproval;\n","path":"index.tsx","target":"components/smoothui/ai-approval/index.tsx","type":"registry:ui"}],"name":"ai-approval","registryDependencies":[],"title":"Ai Approval","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["lucide-react","motion"],"description":"Generated-artifact frame whose preview and code panes swap along a shared axis rather than cross-fading.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { Check, Copy } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { type ReactNode, useEffect, useId, useState } from \"react\";\n\nconst SPRING_DEFAULT = {\n  bounce: 0.1,\n  duration: 0.25,\n  type: \"spring\" as const,\n};\nconst EASE_OUT = [0.23, 1, 0.32, 1] as const;\nconst TRAVEL_PX = 24;\nconst COPIED_RESET_MS = 1600;\n\nexport type AIArtifactPane = \"preview\" | \"code\";\n\nexport type AIArtifactProps = {\n  className?: string;\n  /** The raw form — source, JSON, markup. */\n  code?: ReactNode;\n  /** Plain text handed to the clipboard. Omit to hide the copy action. */\n  copyText?: string;\n  /** Which pane to start on. */\n  defaultPane?: AIArtifactPane;\n  /** The rendered form. */\n  preview?: ReactNode;\n  /** Name the artifact so it can be referred to in conversation. */\n  title: string;\n};\n\nconst PANES: AIArtifactPane[] = [\"preview\", \"code\"];\n\n/**\n * A frame around something the model produced.\n *\n * The two panes travel along a single horizontal axis — preview sits to the left\n * of code, always — so the swap has a direction and, after one go, a memory. A\n * cross-fade between them would leave the user with no sense of where the other\n * pane went, and no expectation of where it will come back from.\n */\nconst AIArtifact = ({\n  className,\n  code,\n  copyText,\n  defaultPane = \"preview\",\n  preview,\n  title,\n}: AIArtifactProps) => {\n  const shouldReduceMotion = useReducedMotion();\n  const [pane, setPane] = useState<AIArtifactPane>(defaultPane);\n  const [hasCopied, setHasCopied] = useState(false);\n  const indicatorId = useId();\n\n  const available = PANES.filter((candidate) =>\n    candidate === \"preview\" ? Boolean(preview) : Boolean(code)\n  );\n\n  useEffect(() => {\n    if (!hasCopied) {\n      return;\n    }\n    const timeout = setTimeout(() => setHasCopied(false), COPIED_RESET_MS);\n    return () => clearTimeout(timeout);\n  }, [hasCopied]);\n\n  const copy = async () => {\n    if (!copyText) {\n      return;\n    }\n    try {\n      await navigator.clipboard.writeText(copyText);\n      setHasCopied(true);\n    } catch {\n      // A blocked clipboard is not worth an error state.\n    }\n  };\n\n  const direction = pane === \"code\" ? 1 : -1;\n\n  return (\n    <div\n      className={cn(\n        \"w-full overflow-hidden rounded-xl border border-border bg-background\",\n        className\n      )}\n    >\n      <div className=\"flex items-center gap-2 border-border border-b px-2 py-1.5\">\n        <span className=\"min-w-0 truncate px-1 font-medium text-foreground text-xs\">\n          {title}\n        </span>\n\n        {available.length > 1 && (\n          <div className=\"ml-auto flex items-center gap-0.5 rounded-lg bg-muted p-0.5\">\n            {available.map((candidate) => (\n              <button\n                aria-selected={pane === candidate}\n                className={cn(\n                  \"relative cursor-pointer rounded-md px-2 py-1 text-xs capitalize transition-colors\",\n                  pane === candidate\n                    ? \"text-foreground\"\n                    : \"text-muted-foreground hover:text-foreground\"\n                )}\n                key={candidate}\n                onClick={() => setPane(candidate)}\n                role=\"tab\"\n                type=\"button\"\n              >\n                {pane === candidate && (\n                  // A single shared element slides between the tabs. Two\n                  // separately styled tabs would just swap colour, which reads\n                  // as two states rather than one selection moving.\n                  <motion.span\n                    className=\"absolute inset-0 rounded-md bg-background shadow-sm\"\n                    layoutId={`${indicatorId}-indicator`}\n                    transition={\n                      shouldReduceMotion ? { duration: 0 } : SPRING_DEFAULT\n                    }\n                  />\n                )}\n                <span className=\"relative\">{candidate}</span>\n              </button>\n            ))}\n          </div>\n        )}\n\n        {copyText ? (\n          <button\n            aria-label={hasCopied ? \"Copied\" : \"Copy\"}\n            className={cn(\n              \"cursor-pointer rounded-md p-1.5 transition-colors\",\n              available.length > 1 ? \"\" : \"ml-auto\",\n              hasCopied\n                ? \"text-foreground\"\n                : \"text-muted-foreground hover:bg-muted hover:text-foreground\"\n            )}\n            onClick={copy}\n            type=\"button\"\n          >\n            {hasCopied ? (\n              <Check aria-hidden=\"true\" size={13} />\n            ) : (\n              <Copy aria-hidden=\"true\" size={13} />\n            )}\n          </button>\n        ) : null}\n      </div>\n\n      <div className=\"relative overflow-hidden\">\n        <AnimatePresence custom={direction} initial={false} mode=\"wait\">\n          <motion.div\n            animate={{ opacity: 1, x: 0 }}\n            custom={direction}\n            exit={\n              shouldReduceMotion\n                ? { opacity: 0, transition: { duration: 0 } }\n                : { opacity: 0, x: -direction * TRAVEL_PX }\n            }\n            initial={\n              shouldReduceMotion\n                ? { opacity: 1, x: 0 }\n                : { opacity: 0, x: direction * TRAVEL_PX }\n            }\n            key={pane}\n            transition={\n              shouldReduceMotion\n                ? { duration: 0 }\n                : { duration: 0.2, ease: EASE_OUT }\n            }\n          >\n            {pane === \"preview\" ? (\n              <div className=\"p-3\">{preview}</div>\n            ) : (\n              <pre className=\"overflow-x-auto p-3 font-mono text-foreground text-xs leading-relaxed\">\n                {code}\n              </pre>\n            )}\n          </motion.div>\n        </AnimatePresence>\n      </div>\n    </div>\n  );\n};\n\nexport default AIArtifact;\n","path":"index.tsx","target":"components/smoothui/ai-artifact/index.tsx","type":"registry:ui"}],"name":"ai-artifact","registryDependencies":[],"title":"Ai Artifact","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["lucide-react","motion"],"description":"An interactive AI branch component for displaying conversation flows.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { ChevronLeftIcon, ChevronRightIcon, Copy, Pencil } from \"lucide-react\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport type { HTMLAttributes, ReactElement, ReactNode } from \"react\";\nimport { createContext, useContext, useEffect, useMemo, useState } from \"react\";\n\ninterface AIBranchContextType {\n  branches: ReactElement[];\n  currentBranch: number;\n  goToNext: () => void;\n  goToPrevious: () => void;\n  setBranches: (branches: ReactElement[]) => void;\n  totalBranches: number;\n}\n\n/**\n * The same two springs every other AI component uses.\n *\n * This file predates that vocabulary and had eight hand-tuned\n * `stiffness`/`damping` pairs — several of which also passed `duration`, which\n * Motion ignores once stiffness is present. Sharing the tokens is what stops\n * `ai-branch` feeling like it came from a different library than `ai-message`.\n */\nconst SPRING_DEFAULT = {\n  bounce: 0.1,\n  duration: 0.25,\n  type: \"spring\" as const,\n};\nconst SPRING_SNAPPY = {\n  bounce: 0,\n  duration: 0.2,\n  type: \"spring\" as const,\n};\n\nconst AIBranchContext = createContext<AIBranchContextType | null>(null);\n\nconst useAIBranch = () => {\n  const context = useContext(AIBranchContext);\n  if (!context) {\n    throw new Error(\"AIBranch components must be used within AIBranch\");\n  }\n  return context;\n};\n\nexport type AIBranchProps = HTMLAttributes<HTMLDivElement> & {\n  defaultBranch?: number;\n  onBranchChange?: (branchIndex: number) => void;\n};\n\nexport const AIBranch = ({\n  defaultBranch = 0,\n  onBranchChange,\n  className,\n  ...props\n}: AIBranchProps) => {\n  const [currentBranch, setCurrentBranch] = useState(defaultBranch);\n  const [branches, setBranches] = useState<ReactElement[]>([]);\n\n  const handleBranchChange = (newBranch: number) => {\n    setCurrentBranch(newBranch);\n    onBranchChange?.(newBranch);\n  };\n\n  const goToPrevious = () => {\n    const newBranch =\n      currentBranch > 0 ? currentBranch - 1 : branches.length - 1;\n    handleBranchChange(newBranch);\n  };\n\n  const goToNext = () => {\n    const newBranch =\n      currentBranch < branches.length - 1 ? currentBranch + 1 : 0;\n    handleBranchChange(newBranch);\n  };\n\n  const contextValue: AIBranchContextType = {\n    branches,\n    currentBranch,\n    goToNext,\n    goToPrevious,\n    setBranches,\n    totalBranches: branches.length,\n  };\n\n  return (\n    <AIBranchContext.Provider value={contextValue}>\n      <div\n        className={cn(\"grid w-full gap-2 [&>div]:pb-0\", className)}\n        {...props}\n      />\n    </AIBranchContext.Provider>\n  );\n};\n\nexport interface AIBranchMessagesProps {\n  children: ReactElement | ReactElement[];\n}\n\nexport const AIBranchMessages = ({ children }: AIBranchMessagesProps) => {\n  const { currentBranch, setBranches, branches } = useAIBranch();\n  const shouldReduceMotion = useReducedMotion();\n  const childrenArray = useMemo(\n    () => (Array.isArray(children) ? children : [children]),\n    [children]\n  );\n\n  // Use useEffect to update branches when they change\n  useEffect(() => {\n    if (branches.length !== childrenArray.length) {\n      setBranches(childrenArray);\n    }\n  }, [childrenArray, branches, setBranches]);\n\n  return childrenArray.map((branch, index) => (\n    <motion.div\n      animate={\n        shouldReduceMotion\n          ? { opacity: index === currentBranch ? 1 : 0 }\n          : {\n              display: index === currentBranch ? \"block\" : \"none\",\n              opacity: index === currentBranch ? 1 : 0,\n              y: index === currentBranch ? 0 : 10,\n            }\n      }\n      className={cn(\n        \"grid gap-2 [&>div]:pb-0\",\n        index === currentBranch ? \"block\" : \"hidden\"\n      )}\n      initial={shouldReduceMotion ? { opacity: 0 } : { opacity: 0, y: 10 }}\n      key={`branch-${index}-${currentBranch}`}\n      transition={shouldReduceMotion ? { duration: 0 } : SPRING_DEFAULT}\n    >\n      {branch}\n    </motion.div>\n  ));\n};\n\nexport type AIBranchSelectorProps = HTMLAttributes<HTMLDivElement> & {\n  from: \"user\" | \"assistant\";\n};\n\nexport const AIBranchSelector = ({\n  className,\n  from,\n  ...props\n}: AIBranchSelectorProps) => {\n  const { totalBranches } = useAIBranch();\n\n  // Don't render if there's only one branch\n  if (totalBranches <= 1) {\n    return null;\n  }\n\n  return (\n    <div\n      className={cn(\n        \"flex items-center gap-2 self-end px-10\",\n        from === \"assistant\" ? \"justify-start\" : \"justify-end\",\n        className\n      )}\n      {...props}\n    />\n  );\n};\n\nexport interface AIBranchPreviousProps {\n  children?: ReactNode;\n  className?: string;\n}\n\nexport const AIBranchPrevious = ({\n  className,\n  children,\n}: AIBranchPreviousProps) => {\n  const { goToPrevious, totalBranches } = useAIBranch();\n  const shouldReduceMotion = useReducedMotion();\n\n  return (\n    <motion.button\n      aria-label=\"Previous branch\"\n      className={cn(\n        \"size-7 shrink-0 cursor-pointer rounded-full text-muted-foreground transition-colors\",\n        \"hover:bg-muted hover:text-foreground\",\n        \"disabled:pointer-events-none disabled:opacity-50\",\n        \"flex items-center justify-center\",\n        className\n      )}\n      disabled={totalBranches <= 1}\n      onClick={goToPrevious}\n      transition={shouldReduceMotion ? { duration: 0 } : SPRING_SNAPPY}\n      type=\"button\"\n      whileHover={shouldReduceMotion ? {} : { scale: 1.05 }}\n      whileTap={shouldReduceMotion ? {} : { scale: 0.95 }}\n    >\n      {children ?? <ChevronLeftIcon size={14} />}\n    </motion.button>\n  );\n};\n\nexport interface AIBranchNextProps {\n  children?: ReactNode;\n  className?: string;\n}\n\nexport const AIBranchNext = ({ className, children }: AIBranchNextProps) => {\n  const { goToNext, totalBranches } = useAIBranch();\n  const shouldReduceMotion = useReducedMotion();\n\n  return (\n    <motion.button\n      aria-label=\"Next branch\"\n      className={cn(\n        \"size-7 shrink-0 cursor-pointer rounded-full text-muted-foreground transition-colors\",\n        \"hover:bg-muted hover:text-foreground\",\n        \"disabled:pointer-events-none disabled:opacity-50\",\n        \"flex items-center justify-center\",\n        className\n      )}\n      disabled={totalBranches <= 1}\n      onClick={goToNext}\n      transition={shouldReduceMotion ? { duration: 0 } : SPRING_SNAPPY}\n      type=\"button\"\n      whileHover={shouldReduceMotion ? {} : { scale: 1.05 }}\n      whileTap={shouldReduceMotion ? {} : { scale: 0.95 }}\n    >\n      {children ?? <ChevronRightIcon size={14} />}\n    </motion.button>\n  );\n};\n\nexport interface AIBranchPageProps {\n  className?: string;\n}\n\nexport const AIBranchPage = ({ className }: AIBranchPageProps) => {\n  const { currentBranch, totalBranches } = useAIBranch();\n\n  return (\n    <span\n      className={cn(\n        \"font-medium text-muted-foreground text-xs tabular-nums\",\n        className\n      )}\n    >\n      {currentBranch + 1} of {totalBranches}\n    </span>\n  );\n};\n\n// Updated type for conversation branches\nexport interface AIBranchData {\n  aiResponse: string;\n  id: string;\n  isActive: boolean;\n  timestamp: Date;\n  userMessage: string;\n}\n\n// Export the type alias for backward compatibility\nexport type AIBranch = AIBranchData;\n\ninterface LegacyAiBranchProps {\n  branches: AIBranchData[];\n  className?: string;\n  onBranchSelect: (branchId: string) => void;\n}\n\n// Updated legacy component to show conversation branches\nexport function LegacyAiBranch({\n  branches,\n  onBranchSelect,\n  className,\n}: LegacyAiBranchProps) {\n  const shouldReduceMotion = useReducedMotion();\n  const [currentBranchIndex, setCurrentBranchIndex] = useState(() =>\n    branches.findIndex((branch) => branch.isActive)\n  );\n\n  const activeBranch = branches[currentBranchIndex];\n\n  const goToPrevious = () => {\n    const newIndex =\n      currentBranchIndex > 0 ? currentBranchIndex - 1 : branches.length - 1;\n    setCurrentBranchIndex(newIndex);\n    onBranchSelect(branches[newIndex].id);\n  };\n\n  const goToNext = () => {\n    const newIndex =\n      currentBranchIndex < branches.length - 1 ? currentBranchIndex + 1 : 0;\n    setCurrentBranchIndex(newIndex);\n    onBranchSelect(branches[newIndex].id);\n  };\n\n  return (\n    <div className={cn(\"w-full max-w-2xl\", className)}>\n      {/* Active Branch Display */}\n      {activeBranch ? (\n        <motion.div\n          animate={{ opacity: 1, y: 0 }}\n          className=\"mb-4 space-y-4\"\n          initial={{ opacity: 0, y: 10 }}\n          transition={SPRING_DEFAULT}\n        >\n          {/* User Message with Branch Navigation */}\n          <div className=\"flex justify-end\">\n            <div className=\"flex flex-col items-end gap-2\">\n              <div className=\"max-w-full rounded-2xl rounded-br-md bg-foreground px-3.5 py-2.5 text-background\">\n                <p className=\"text-sm leading-relaxed\">\n                  {activeBranch.userMessage}\n                </p>\n              </div>\n\n              {/* Branch Navigation Controls */}\n              {branches.length > 1 && (\n                <div className=\"flex items-center gap-1\">\n                  <motion.button\n                    aria-label=\"Copy message\"\n                    className={cn(\n                      \"size-6 shrink-0 cursor-pointer rounded text-foreground/70 transition-colors\",\n                      \"hover:bg-muted hover:text-foreground\",\n                      \"flex items-center justify-center\"\n                    )}\n                    transition={\n                      shouldReduceMotion ? { duration: 0 } : SPRING_SNAPPY\n                    }\n                    type=\"button\"\n                    whileHover={shouldReduceMotion ? {} : { scale: 1.05 }}\n                    whileTap={shouldReduceMotion ? {} : { scale: 0.95 }}\n                  >\n                    <Copy className=\"h-3 w-3\" />\n                  </motion.button>\n\n                  <motion.button\n                    aria-label=\"Edit message\"\n                    className={cn(\n                      \"size-6 shrink-0 cursor-pointer rounded text-foreground/70 transition-colors\",\n                      \"hover:bg-muted hover:text-foreground\",\n                      \"flex items-center justify-center\"\n                    )}\n                    transition={\n                      shouldReduceMotion ? { duration: 0 } : SPRING_SNAPPY\n                    }\n                    type=\"button\"\n                    whileHover={shouldReduceMotion ? {} : { scale: 1.05 }}\n                    whileTap={shouldReduceMotion ? {} : { scale: 0.95 }}\n                  >\n                    <Pencil className=\"h-3 w-3\" />\n                  </motion.button>\n\n                  <motion.button\n                    aria-label=\"Previous branch\"\n                    className={cn(\n                      \"size-6 shrink-0 cursor-pointer rounded text-foreground/70 transition-colors\",\n                      \"hover:bg-muted hover:text-foreground\",\n                      \"disabled:pointer-events-none disabled:opacity-50\",\n                      \"flex items-center justify-center\"\n                    )}\n                    disabled={branches.length <= 1}\n                    onClick={goToPrevious}\n                    transition={\n                      shouldReduceMotion ? { duration: 0 } : SPRING_SNAPPY\n                    }\n                    type=\"button\"\n                    whileHover={shouldReduceMotion ? {} : { scale: 1.05 }}\n                    whileTap={shouldReduceMotion ? {} : { scale: 0.95 }}\n                  >\n                    <ChevronLeftIcon size={12} />\n                  </motion.button>\n\n                  <span className=\"font-medium text-foreground/70 text-xs tabular-nums\">\n                    {currentBranchIndex + 1}/{branches.length}\n                  </span>\n\n                  <motion.button\n                    aria-label=\"Next branch\"\n                    className={cn(\n                      \"size-6 shrink-0 cursor-pointer rounded text-foreground/70 transition-colors\",\n                      \"hover:bg-muted hover:text-foreground\",\n                      \"disabled:pointer-events-none disabled:opacity-50\",\n                      \"flex items-center justify-center\"\n                    )}\n                    disabled={branches.length <= 1}\n                    onClick={goToNext}\n                    transition={\n                      shouldReduceMotion ? { duration: 0 } : SPRING_SNAPPY\n                    }\n                    type=\"button\"\n                    whileHover={shouldReduceMotion ? {} : { scale: 1.05 }}\n                    whileTap={shouldReduceMotion ? {} : { scale: 0.95 }}\n                  >\n                    <ChevronRightIcon size={12} />\n                  </motion.button>\n                </div>\n              )}\n            </div>\n          </div>\n\n          {/* AI Response */}\n          <div className=\"flex justify-start\">\n            <div className=\"max-w-[80%] rounded-2xl rounded-bl-md bg-muted px-3.5 py-2.5\">\n              <p className=\"text-foreground text-sm leading-relaxed\">\n                {activeBranch.aiResponse}\n              </p>\n            </div>\n          </div>\n        </motion.div>\n      ) : null}\n    </div>\n  );\n}\n\n// Export the legacy component as the default for backward compatibility\nexport { LegacyAiBranch as AiBranch };\n\n// Add default export for lazy loading\nexport default LegacyAiBranch;\n","path":"index.tsx","target":"components/smoothui/ai-branch/index.tsx","type":"registry:ui"}],"name":"ai-branch","registryDependencies":[],"title":"Ai Branch","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["lucide-react","motion"],"description":"Inline citation pill that opens an origin-aware source card, anchored to the pill it came from.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { ArrowUpRight, Globe } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { type ReactNode, useEffect, useRef, useState } from \"react\";\n\nconst SPRING_DEFAULT = {\n  bounce: 0.1,\n  duration: 0.25,\n  type: \"spring\" as const,\n};\n/** Grace period so the pointer can cross the gap between pill and card. */\nconst CLOSE_DELAY_MS = 120;\nconst CARD_WIDTH_PX = 288;\n\nexport type AICitationProps = {\n  className?: string;\n  /** Excerpt or description shown in the card. */\n  description?: string;\n  /**\n   * The source's mark for the card header — a real isotype or an `<img>`.\n   *\n   * A node rather than a URL so a brand's own SVG can be passed straight in.\n   */\n  favicon?: ReactNode;\n  /** The number or short label shown in the pill. */\n  label: string | number;\n  title: string;\n  url: string;\n};\n\nconst hostOf = (url: string): string => {\n  try {\n    return new URL(url).hostname.replace(/^www\\./, \"\");\n  } catch {\n    return url;\n  }\n};\n\n/**\n * An inline citation that can be peeked at.\n *\n * The card scales up **from the pill** rather than fading in centred: with a\n * transform origin at the bottom of the marker, the growth points back at what\n * was clicked, so the pointer never loses the thread. It opens on hover and on\n * focus, and closes on Escape — a hover-only preview is unreachable by keyboard\n * and unusable on touch.\n */\nconst AICitation = ({\n  className,\n  description,\n  favicon,\n  label,\n  title,\n  url,\n}: AICitationProps) => {\n  const shouldReduceMotion = useReducedMotion();\n  const [isOpen, setIsOpen] = useState(false);\n  const closeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n  const open = () => {\n    if (closeTimeoutRef.current) {\n      clearTimeout(closeTimeoutRef.current);\n      closeTimeoutRef.current = null;\n    }\n    setIsOpen(true);\n  };\n\n  const scheduleClose = () => {\n    closeTimeoutRef.current = setTimeout(\n      () => setIsOpen(false),\n      CLOSE_DELAY_MS\n    );\n  };\n\n  useEffect(\n    () => () => {\n      if (closeTimeoutRef.current) {\n        clearTimeout(closeTimeoutRef.current);\n      }\n    },\n    []\n  );\n\n  useEffect(() => {\n    if (!isOpen) {\n      return;\n    }\n    const handle = (event: KeyboardEvent) => {\n      if (event.key === \"Escape\") {\n        setIsOpen(false);\n      }\n    };\n    window.addEventListener(\"keydown\", handle);\n    return () => window.removeEventListener(\"keydown\", handle);\n  }, [isOpen]);\n\n  return (\n    // biome-ignore lint/a11y/noStaticElementInteractions: the wrapper only tracks hover and focus to reveal a preview of the link it contains; the link itself is the interactive element and works without it\n    // biome-ignore lint/a11y/noNoninteractiveElementInteractions: same — progressive disclosure around a real anchor, not an interactive span\n    <span\n      className={cn(\"relative inline-block align-super\", className)}\n      onBlur={scheduleClose}\n      onFocus={open}\n      onMouseEnter={open}\n      onMouseLeave={scheduleClose}\n    >\n      <a\n        aria-describedby={isOpen ? `${url}-card` : undefined}\n        className=\"inline-flex h-4 min-w-4 items-center justify-center rounded-full border border-border bg-muted px-1 font-medium text-[10px] text-muted-foreground no-underline transition-colors hover:border-foreground/30 hover:text-foreground\"\n        href={url}\n        rel=\"noopener noreferrer\"\n        target=\"_blank\"\n      >\n        {label}\n      </a>\n\n      <AnimatePresence>\n        {isOpen ? (\n          <motion.span\n            animate={{ opacity: 1, scale: 1, y: 0 }}\n            className=\"absolute bottom-full left-1/2 z-50 mb-1.5 block rounded-xl border border-border bg-background p-3 text-left align-baseline shadow-lg\"\n            exit={\n              shouldReduceMotion\n                ? { opacity: 0, transition: { duration: 0 } }\n                : { opacity: 0, scale: 0.94, y: 4 }\n            }\n            id={`${url}-card`}\n            initial={\n              shouldReduceMotion\n                ? { opacity: 1, scale: 1, y: 0 }\n                : { opacity: 0, scale: 0.94, y: 4 }\n            }\n            style={{\n              // Origin at the bottom centre — where the pill is — so the card\n              // grows out of the marker instead of appearing over it.\n              transformOrigin: \"bottom center\",\n              translateX: \"-50%\",\n              width: CARD_WIDTH_PX,\n            }}\n            transition={shouldReduceMotion ? { duration: 0 } : SPRING_DEFAULT}\n          >\n            <span className=\"mb-1 flex items-center gap-1.5\">\n              <span className=\"flex size-3.5 shrink-0 items-center justify-center overflow-hidden rounded-sm text-muted-foreground *:size-full *:fill-foreground *:object-cover\">\n                {favicon ?? <Globe aria-hidden=\"true\" size={11} />}\n              </span>\n              <span className=\"truncate text-muted-foreground text-xs\">\n                {hostOf(url)}\n              </span>\n              <ArrowUpRight\n                aria-hidden=\"true\"\n                className=\"ml-auto text-muted-foreground\"\n                size={12}\n              />\n            </span>\n\n            <span className=\"block font-medium text-foreground text-sm leading-snug\">\n              {title}\n            </span>\n\n            {description ? (\n              <span className=\"mt-1 block text-muted-foreground text-xs leading-relaxed\">\n                {description}\n              </span>\n            ) : null}\n          </motion.span>\n        ) : null}\n      </AnimatePresence>\n    </span>\n  );\n};\n\nexport default AICitation;\n","path":"index.tsx","target":"components/smoothui/ai-citation/index.tsx","type":"registry:ui"}],"name":"ai-citation","registryDependencies":[],"title":"Ai Citation","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["lucide-react","motion"],"description":"Context window meter whose ring fills and shifts hue at the warning threshold, never changing size.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useState } from \"react\";\n\nconst SPRING_DEFAULT = {\n  bounce: 0.1,\n  duration: 0.25,\n  type: \"spring\" as const,\n};\nconst EASE_OUT = [0.23, 1, 0.32, 1] as const;\nconst VIEWBOX = 32;\nconst CENTER = VIEWBOX / 2;\nconst RADIUS = 13;\nconst STROKE_WIDTH = 3;\n/** Fraction of the window at which the ring changes hue. */\nconst DEFAULT_WARNING_AT = 0.8;\nconst DEFAULT_DANGER_AT = 0.95;\nconst WARNING_COLOR = \"oklch(78% 0.16 75)\";\nconst DANGER_COLOR = \"oklch(63% 0.21 25)\";\n\nexport type AIContextBreakdownItem = {\n  label: string;\n  tokens: number;\n};\n\nexport type AIContextMeterProps = {\n  /** Optional split of what is filling the window. */\n  breakdown?: AIContextBreakdownItem[];\n  className?: string;\n  /** Fraction at which the ring turns red. */\n  dangerAt?: number;\n  /** Total size of the context window, in tokens. */\n  limit: number;\n  /** Tokens currently used. */\n  used: number;\n  /** Fraction at which the ring turns amber. */\n  warningAt?: number;\n};\n\nconst COMPACT_THRESHOLD = 1000;\nconst MILLION = 1_000_000;\n\n/** Below this many thousands, keep a decimal — \"2k\" for 1,800 is a lie. */\nconst DECIMAL_BELOW = 10 * COMPACT_THRESHOLD;\n\nconst formatTokens = (tokens: number): string => {\n  if (tokens >= MILLION) {\n    return `${(tokens / MILLION).toFixed(1)}M`;\n  }\n  if (tokens >= DECIMAL_BELOW) {\n    return `${Math.round(tokens / COMPACT_THRESHOLD)}k`;\n  }\n  if (tokens >= COMPACT_THRESHOLD) {\n    return `${(tokens / COMPACT_THRESHOLD).toFixed(1)}k`;\n  }\n  return String(tokens);\n};\n\n/**\n * How much of the context window is gone.\n *\n * Crossing a threshold changes the **hue**, never the size. Growing the ring at\n * the warning point would read as progress — as something filling up nicely —\n * which is the opposite of the message. The geometry stays put and the colour\n * does the talking.\n */\nconst AIContextMeter = ({\n  breakdown,\n  className,\n  dangerAt = DEFAULT_DANGER_AT,\n  limit,\n  used,\n  warningAt = DEFAULT_WARNING_AT,\n}: AIContextMeterProps) => {\n  const shouldReduceMotion = useReducedMotion();\n  const [isOpen, setIsOpen] = useState(false);\n\n  const fraction = limit > 0 ? Math.min(1, Math.max(0, used / limit)) : 0;\n  const percent = Math.round(fraction * 100);\n\n  const color = (() => {\n    if (fraction >= dangerAt) {\n      return DANGER_COLOR;\n    }\n    if (fraction >= warningAt) {\n      return WARNING_COLOR;\n    }\n    return \"currentColor\";\n  })();\n\n  const hasBreakdown = Boolean(breakdown?.length);\n\n  return (\n    <div className={cn(\"relative inline-block\", className)}>\n      <button\n        aria-expanded={hasBreakdown ? isOpen : undefined}\n        aria-label={`Context window ${percent}% used, ${formatTokens(used)} of ${formatTokens(limit)} tokens`}\n        className=\"flex cursor-pointer items-center gap-1.5 rounded-lg px-1 py-0.5 text-muted-foreground text-xs transition-colors hover:text-foreground\"\n        disabled={!hasBreakdown}\n        onBlur={() => setIsOpen(false)}\n        onClick={() => setIsOpen((current) => !current)}\n        onFocus={() => hasBreakdown && setIsOpen(true)}\n        onMouseEnter={() => hasBreakdown && setIsOpen(true)}\n        onMouseLeave={() => setIsOpen(false)}\n        type=\"button\"\n      >\n        <svg\n          aria-hidden=\"true\"\n          className=\"size-4 -rotate-90\"\n          viewBox={`0 0 ${VIEWBOX} ${VIEWBOX}`}\n        >\n          <circle\n            cx={CENTER}\n            cy={CENTER}\n            fill=\"none\"\n            r={RADIUS}\n            stroke=\"currentColor\"\n            strokeOpacity={0.2}\n            strokeWidth={STROKE_WIDTH}\n          />\n          {/* pathLength normalises the dash maths, so the fill is just the\n              fraction — no circumference arithmetic to get wrong. */}\n          <motion.circle\n            animate={{\n              stroke: color,\n              strokeDasharray: `${fraction} ${1 - fraction}`,\n            }}\n            cx={CENTER}\n            cy={CENTER}\n            fill=\"none\"\n            pathLength={1}\n            r={RADIUS}\n            strokeLinecap=\"round\"\n            strokeWidth={STROKE_WIDTH}\n            transition={shouldReduceMotion ? { duration: 0 } : SPRING_DEFAULT}\n          />\n        </svg>\n\n        <span className=\"tabular-nums\">\n          {formatTokens(used)}/{formatTokens(limit)}\n        </span>\n      </button>\n\n      <AnimatePresence>\n        {isOpen && hasBreakdown ? (\n          <motion.div\n            animate={{ opacity: 1, scale: 1, y: 0 }}\n            className=\"absolute bottom-full left-0 z-50 mb-1.5 w-56 rounded-xl border border-border bg-background p-2.5 shadow-lg\"\n            exit={\n              shouldReduceMotion\n                ? { opacity: 0, transition: { duration: 0 } }\n                : { opacity: 0, scale: 0.96, y: 4 }\n            }\n            initial={\n              shouldReduceMotion\n                ? { opacity: 1, scale: 1, y: 0 }\n                : { opacity: 0, scale: 0.96, y: 4 }\n            }\n            style={{ transformOrigin: \"bottom left\" }}\n            transition={\n              shouldReduceMotion\n                ? { duration: 0 }\n                : { duration: 0.18, ease: EASE_OUT }\n            }\n          >\n            <ul className=\"list-none space-y-1\">\n              {breakdown?.map((item) => (\n                <li\n                  className=\"flex items-baseline justify-between gap-3 text-xs\"\n                  key={item.label}\n                >\n                  <span className=\"truncate text-muted-foreground\">\n                    {item.label}\n                  </span>\n                  <span className=\"shrink-0 text-foreground tabular-nums\">\n                    {formatTokens(item.tokens)}\n                  </span>\n                </li>\n              ))}\n            </ul>\n          </motion.div>\n        ) : null}\n      </AnimatePresence>\n    </div>\n  );\n};\n\nexport default AIContextMeter;\n","path":"index.tsx","target":"components/smoothui/ai-context-meter/index.tsx","type":"registry:ui"}],"name":"ai-context-meter","registryDependencies":[],"title":"Ai Context Meter","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["lucide-react","motion"],"description":"Thread scroll container that follows the bottom only while the reader is already there, offering a jump-to-latest pill when it stops.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { ArrowDown } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport {\n  type ReactNode,\n  useCallback,\n  useEffect,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from \"react\";\n\nconst SPRING_DEFAULT = {\n  bounce: 0.1,\n  duration: 0.25,\n  type: \"spring\" as const,\n};\n/**\n * How close to the bottom still counts as \"at the bottom\".\n *\n * Zero would break on fractional scroll heights, and anything large would keep\n * yanking the view down while someone is reading a few lines up.\n */\nconst BOTTOM_THRESHOLD_PX = 48;\n\nexport type AIConversationProps = {\n  children: ReactNode;\n  className?: string;\n  /**\n   * Changes whenever content grows — message count, or streamed text length.\n   * Used to decide when to follow the bottom.\n   */\n  contentKey?: string | number;\n};\n\n/**\n * Scroll container for a thread.\n *\n * It follows the bottom **only while the reader is already there**. Scrolling up\n * during a stream is an explicit act — the user wants to read something — and\n * dragging them back down is the single most common way chat UIs become\n * unusable. When it stops following, a pill appears to offer the trip back.\n */\nconst AIConversation = ({\n  children,\n  className,\n  contentKey,\n}: AIConversationProps) => {\n  const shouldReduceMotion = useReducedMotion();\n  const viewportRef = useRef<HTMLDivElement | null>(null);\n  const [isPinned, setIsPinned] = useState(true);\n\n  const measure = useCallback(() => {\n    const viewport = viewportRef.current;\n    if (!viewport) {\n      return;\n    }\n    const distance =\n      viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight;\n    setIsPinned(distance <= BOTTOM_THRESHOLD_PX);\n  }, []);\n\n  const scrollToBottom = useCallback((behavior: ScrollBehavior) => {\n    const viewport = viewportRef.current;\n    if (!viewport) {\n      return;\n    }\n    // Element.scrollTo is missing in jsdom and in a few older engines, and it is\n    // not worth throwing over — assigning scrollTop lands in the same place,\n    // just without the smoothing.\n    if (typeof viewport.scrollTo === \"function\") {\n      viewport.scrollTo({ behavior, top: viewport.scrollHeight });\n    } else {\n      viewport.scrollTop = viewport.scrollHeight;\n    }\n    setIsPinned(true);\n  }, []);\n\n  // Layout effect, so the jump happens in the same frame the content grew and\n  // the reader never sees a flash of the pre-scroll position.\n  useLayoutEffect(() => {\n    if (isPinned) {\n      scrollToBottom(shouldReduceMotion ? \"auto\" : \"smooth\");\n    }\n  }, [contentKey, isPinned, scrollToBottom, shouldReduceMotion]);\n\n  useEffect(() => {\n    const viewport = viewportRef.current;\n    if (!viewport) {\n      return;\n    }\n    // A resize observer catches content that grows without contentKey changing —\n    // an image finishing loading, a details element opening.\n    const observer = new ResizeObserver(measure);\n    for (const child of Array.from(viewport.children)) {\n      observer.observe(child);\n    }\n    return () => observer.disconnect();\n  }, [measure]);\n\n  return (\n    <div className={cn(\"relative min-h-0 w-full\", className)}>\n      <div\n        className=\"h-full overflow-y-auto overscroll-contain\"\n        onScroll={measure}\n        ref={viewportRef}\n      >\n        {children}\n      </div>\n\n      <AnimatePresence initial={false}>\n        {!isPinned && (\n          <motion.button\n            animate={{ opacity: 1, scale: 1, y: 0 }}\n            aria-label=\"Jump to latest\"\n            className=\"absolute inset-x-0 bottom-3 mx-auto flex w-fit cursor-pointer items-center gap-1.5 rounded-full border border-border bg-background/90 py-1.5 pr-3 pl-2.5 text-foreground text-xs shadow-sm backdrop-blur\"\n            exit={\n              shouldReduceMotion\n                ? { opacity: 0, transition: { duration: 0 } }\n                : { opacity: 0, scale: 0.96, y: 8 }\n            }\n            initial={\n              shouldReduceMotion\n                ? { opacity: 1, scale: 1, y: 0 }\n                : { opacity: 0, scale: 0.96, y: 8 }\n            }\n            onClick={() =>\n              scrollToBottom(shouldReduceMotion ? \"auto\" : \"smooth\")\n            }\n            transition={shouldReduceMotion ? { duration: 0 } : SPRING_DEFAULT}\n            type=\"button\"\n          >\n            <ArrowDown aria-hidden=\"true\" size={13} />\n            Jump to latest\n          </motion.button>\n        )}\n      </AnimatePresence>\n    </div>\n  );\n};\n\nexport default AIConversation;\n","path":"index.tsx","target":"components/smoothui/ai-conversation/index.tsx","type":"registry:ui"}],"name":"ai-conversation","registryDependencies":[],"title":"Ai Conversation","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"Shared AI state contract, per-state motion presets and microphone amplitude hooks for SmoothUI AI components.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport {\n  type MotionValue,\n  useAnimationFrame,\n  useMotionValue,\n} from \"motion/react\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\n\n/**\n * The single state contract shared by every SmoothUI AI component.\n *\n * Passing the same value to an orb, a prompt input and a tool card makes the\n * whole surface move as one organism instead of a set of independent widgets.\n */\nexport type AIState =\n  | \"idle\"\n  | \"listening\"\n  | \"thinking\"\n  | \"streaming\"\n  | \"done\"\n  | \"error\";\n\n/**\n * A behavioural hint each component expresses **in its own material**.\n *\n * Deliberately not an overlay. Bolting one shared graphic onto every orb makes\n * four different materials look like the same widget wearing a costume, and it\n * pushes literal iconography (a checkmark, a warning ring) onto pieces that are\n * decorative by nature. Instead: a shader warps, a canvas ring loses harmonics,\n * a character changes expression. Same vocabulary, different flesh.\n */\nexport type AIStateMotif =\n  | \"breathe\"\n  | \"receive\"\n  | \"scan\"\n  | \"pulse\"\n  | \"ping\"\n  | \"fault\";\n\n/** Semantic accent applied on top of the component's own palette. */\nexport type AIStateAccent = \"success\" | \"danger\" | null;\n\n/** Motion parameters a component reads to render a given {@link AIState}. */\nexport type AIStateMotion = {\n  /** Semantic colour override, so status is carried by hue and not only motion. */\n  accent: AIStateAccent;\n  /** Outer bloom strength, 0–1. */\n  glow: number;\n  /** Palette hue rotation in degrees. Small shifts read as a mood change. */\n  hueRotate: number;\n  /** Overall motion energy, 0–1. Scales ambient loops and displacement. */\n  intensity: number;\n  /** Behavioural hint; each component expresses it in its own material. */\n  motif: AIStateMotif;\n  /** Seconds between discrete pulses, for rhythmic behaviour. */\n  pulseSeconds: number;\n  /**\n   * How far a shader's domain warp pushes the field, 0–1. Low values read as a\n   * calm surface; high values churn without the silhouette growing.\n   */\n  turbulence: number;\n  /** Revolutions per second of the noise field — the slow tumble. */\n  tumble: number;\n  /** How much external amplitude reaches the surface, 0 = ignore it. */\n  reactivity: number;\n  /** Chroma multiplier. Below 1 desaturates. */\n  saturation: number;\n  /** Resting scale of the surface, 1 = no change. */\n  scale: number;\n  /** Ambient loop speed multiplier, 1 = the component's base tempo. */\n  speed: number;\n};\n\n/**\n * Per-state presets.\n *\n * Deliberate choices worth keeping:\n * - `thinking` has `scale: 1` — internal churn only, so layout stays calm while\n *   the model works. Growing the surface here makes pages feel unstable.\n * - `error` desaturates instead of growing, so it reads as a state change\n *   rather than an attention grab.\n * - `done` overshoots once; components are expected to settle back to `idle`.\n * - Each state owns a distinct `motif`, so the six states are told apart by\n *   what moves, not by how fast it moves.\n */\nexport const AI_STATE_MOTION: Record<AIState, AIStateMotion> = {\n  done: {\n    accent: \"success\",\n    glow: 0.7,\n    hueRotate: 0,\n    intensity: 0.4,\n    motif: \"ping\",\n    pulseSeconds: 0.65,\n    reactivity: 0,\n    saturation: 1,\n    scale: 1.1,\n    speed: 0.8,\n    // The field settles almost flat — stillness is what reads as \"finished\".\n    tumble: 0.02,\n    turbulence: 0.08,\n  },\n  error: {\n    accent: \"danger\",\n    glow: 0.25,\n    hueRotate: 0,\n    intensity: 0.5,\n    motif: \"fault\",\n    pulseSeconds: 0.9,\n    reactivity: 0,\n    saturation: 0.3,\n    scale: 0.96,\n    speed: 1,\n    // Tumble stalls while turbulence stays mid: the surface twitches in place\n    // instead of flowing, which is what a fault feels like.\n    tumble: 0,\n    turbulence: 0.55,\n  },\n  idle: {\n    accent: null,\n    glow: 0.15,\n    hueRotate: 0,\n    intensity: 0.3,\n    motif: \"breathe\",\n    pulseSeconds: 4.5,\n    reactivity: 0,\n    saturation: 0.75,\n    scale: 0.94,\n    speed: 0.6,\n    tumble: 0.012,\n    turbulence: 0.14,\n  },\n  listening: {\n    accent: null,\n    glow: 0.6,\n    hueRotate: 0,\n    intensity: 0.75,\n    motif: \"receive\",\n    pulseSeconds: 1.6,\n    reactivity: 1,\n    saturation: 1.05,\n    scale: 1.06,\n    speed: 1,\n    tumble: 0.03,\n    turbulence: 0.42,\n  },\n  streaming: {\n    accent: null,\n    glow: 0.45,\n    hueRotate: -10,\n    intensity: 0.6,\n    motif: \"pulse\",\n    pulseSeconds: 1.25,\n    reactivity: 0.6,\n    saturation: 1,\n    scale: 1.02,\n    speed: 1.4,\n    tumble: 0.06,\n    turbulence: 0.5,\n  },\n  thinking: {\n    accent: null,\n    glow: 0.35,\n    hueRotate: 18,\n    intensity: 1,\n    motif: \"scan\",\n    pulseSeconds: 1.1,\n    reactivity: 0.15,\n    saturation: 1,\n    scale: 1,\n    speed: 2.4,\n    // High churn, unchanged silhouette: the work is visible without the orb\n    // growing and unsettling the layout around it.\n    tumble: 0.14,\n    turbulence: 0.95,\n  },\n};\n\n/** Semantic accents. Deliberately not tokens — orbs render outside a theme. */\nexport const AI_ACCENT_COLORS: Record<\"success\" | \"danger\", string> = {\n  danger: \"oklch(63% 0.21 25)\",\n  success: \"oklch(72% 0.17 150)\",\n};\n\n/**\n * Colour used by a state's overlay motif: the semantic accent when the state\n * has one, otherwise the component's own secondary colour.\n */\nexport const getAIStateAccentColor = (\n  state: AIState | undefined,\n  fallback: string\n): string => {\n  const accent = AI_STATE_MOTION[state ?? \"idle\"]?.accent;\n  return accent ? AI_ACCENT_COLORS[accent] : fallback;\n};\n\n/** Motion preset for a state, falling back to `idle` for unknown values. */\nexport const getAIStateMotion = (state: AIState | undefined): AIStateMotion =>\n  AI_STATE_MOTION[state ?? \"idle\"] ?? AI_STATE_MOTION.idle;\n\n/**\n * Amplitude accepted by every reactive AI component.\n *\n * A `MotionValue` is the preferred form: it updates outside React, so a 60fps\n * audio signal never triggers a re-render.\n */\nexport type AIAmplitude = number | MotionValue<number> | undefined;\n\nconst isMotionValue = (value: AIAmplitude): value is MotionValue<number> =>\n  typeof value === \"object\" && value !== null && \"get\" in value;\n\n/**\n * Normalises the `amplitude` prop into a stable `MotionValue<number>` so\n * component internals only deal with one shape.\n */\nexport const useAmplitudeValue = (\n  amplitude: AIAmplitude\n): MotionValue<number> => {\n  const fallback = useMotionValue(0);\n  const numeric = typeof amplitude === \"number\" ? amplitude : null;\n\n  useEffect(() => {\n    if (numeric !== null) {\n      fallback.set(numeric);\n    }\n  }, [numeric, fallback]);\n\n  return isMotionValue(amplitude) ? amplitude : fallback;\n};\n\nexport type AudioAmplitudeStatus =\n  | \"idle\"\n  | \"requesting\"\n  | \"active\"\n  | \"denied\"\n  | \"unsupported\";\n\nexport type UseAudioAmplitudeOptions = {\n  /** Request microphone access as soon as the hook mounts. */\n  autoStart?: boolean;\n  /**\n   * Envelope smoothing, 0–1. Higher is smoother and lazier; the default keeps\n   * attack snappy so an orb reacts on the first syllable.\n   */\n  smoothing?: number;\n  /** FFT size handed to the analyser node. Must be a power of two. */\n  fftSize?: number;\n};\n\nexport type UseAudioAmplitudeResult = {\n  /** Smoothed RMS level, 0–1, as a `MotionValue`. */\n  amplitude: MotionValue<number>;\n  status: AudioAmplitudeStatus;\n  start: () => Promise<void>;\n  stop: () => void;\n};\n\nconst DEFAULT_SMOOTHING = 0.55;\nconst DEFAULT_FFT_SIZE = 512;\n/** Raw RMS rarely exceeds ~0.3 for speech, so normalise into a usable 0–1. */\nconst RMS_TO_UNIT = 3.2;\n/** Attack is faster than release so peaks land immediately and decay gently. */\nconst ATTACK_FACTOR = 0.35;\n\n/**\n * Reads microphone loudness as a 0–1 `MotionValue`.\n *\n * SSR-safe, permission-aware, and silent on failure — a denied prompt leaves\n * the amplitude at 0 so the consuming component simply falls back to its\n * ambient animation.\n */\nexport const useAudioAmplitude = (\n  options: UseAudioAmplitudeOptions = {}\n): UseAudioAmplitudeResult => {\n  const {\n    autoStart = false,\n    smoothing = DEFAULT_SMOOTHING,\n    fftSize = DEFAULT_FFT_SIZE,\n  } = options;\n\n  const amplitude = useMotionValue(0);\n  const [status, setStatus] = useState<AudioAmplitudeStatus>(\"idle\");\n\n  const contextRef = useRef<AudioContext | null>(null);\n  const streamRef = useRef<MediaStream | null>(null);\n  const analyserRef = useRef<AnalyserNode | null>(null);\n  const bufferRef = useRef<Float32Array<ArrayBuffer> | null>(null);\n\n  const stop = useCallback(() => {\n    for (const track of streamRef.current?.getTracks() ?? []) {\n      track.stop();\n    }\n    streamRef.current = null;\n    analyserRef.current = null;\n    bufferRef.current = null;\n    contextRef.current?.close();\n    contextRef.current = null;\n    amplitude.set(0);\n    setStatus(\"idle\");\n  }, [amplitude]);\n\n  const start = useCallback(async () => {\n    if (analyserRef.current) {\n      return;\n    }\n\n    const AudioContextCtor =\n      typeof window === \"undefined\"\n        ? undefined\n        : (window.AudioContext ??\n          (window as unknown as { webkitAudioContext?: typeof AudioContext })\n            .webkitAudioContext);\n\n    if (!(AudioContextCtor && navigator.mediaDevices?.getUserMedia)) {\n      setStatus(\"unsupported\");\n      return;\n    }\n\n    setStatus(\"requesting\");\n\n    try {\n      const stream = await navigator.mediaDevices.getUserMedia({ audio: true });\n      const context = new AudioContextCtor();\n      const analyser = context.createAnalyser();\n      analyser.fftSize = fftSize;\n      context.createMediaStreamSource(stream).connect(analyser);\n\n      streamRef.current = stream;\n      contextRef.current = context;\n      analyserRef.current = analyser;\n      bufferRef.current = new Float32Array(analyser.fftSize);\n      setStatus(\"active\");\n    } catch {\n      setStatus(\"denied\");\n    }\n  }, [fftSize]);\n\n  useEffect(() => {\n    if (autoStart) {\n      start();\n    }\n    return stop;\n  }, [autoStart, start, stop]);\n\n  useAnimationFrame(() => {\n    const analyser = analyserRef.current;\n    const buffer = bufferRef.current;\n    if (!(analyser && buffer)) {\n      return;\n    }\n\n    analyser.getFloatTimeDomainData(buffer);\n\n    let sumOfSquares = 0;\n    for (const sample of buffer) {\n      sumOfSquares += sample * sample;\n    }\n    const rms = Math.sqrt(sumOfSquares / buffer.length);\n    const target = Math.min(1, rms * RMS_TO_UNIT);\n\n    const previous = amplitude.get();\n    const factor = target > previous ? smoothing * ATTACK_FACTOR : smoothing;\n    amplitude.set(previous + (target - previous) * (1 - factor));\n  });\n\n  return { amplitude, start, status, stop };\n};\n\n/**\n * Amplitude generator for demos, docs and previews — no microphone involved.\n *\n * Produces a plausible speech-like envelope whose energy follows the current\n * {@link AIState}, so every example can show the reactive behaviour without\n * asking the visitor for permissions.\n */\nexport const useSimulatedAmplitude = (\n  state: AIState = \"idle\"\n): MotionValue<number> => {\n  const amplitude = useMotionValue(0);\n  const motion = getAIStateMotion(state);\n\n  useAnimationFrame((time) => {\n    const t = time / 1000;\n    // Three detuned sines read as organic; a single sine reads as a metronome.\n    const envelope =\n      0.5 +\n      0.3 * Math.sin(t * 2.1 * motion.speed) +\n      0.14 * Math.sin(t * 5.3 * motion.speed + 1.7) +\n      0.06 * Math.sin(t * 11.7 * motion.speed + 0.4);\n\n    amplitude.set(Math.min(1, Math.max(0, envelope * motion.intensity)));\n  });\n\n  return amplitude;\n};\n","path":"index.tsx","target":"components/smoothui/ai-core/index.tsx","type":"registry:ui"}],"name":"ai-core","registryDependencies":[],"title":"Ai Core","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["lucide-react","motion"],"description":"Proposed code or data edit where added lines wipe in from the left and rejected lines collapse to nothing.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useState } from \"react\";\n\nconst SPRING_DEFAULT = {\n  bounce: 0.1,\n  duration: 0.25,\n  type: \"spring\" as const,\n};\nconst EASE_OUT = [0.23, 1, 0.32, 1] as const;\nconst LINE_STAGGER = 0.02;\nconst WIPE_DURATION = 0.28;\nconst FLASH_DURATION = 0.35;\nconst SUCCESS_TINT = \"oklch(72% 0.17 150 / 0.14)\";\nconst DANGER_TINT = \"oklch(63% 0.21 25 / 0.12)\";\n\nexport type AIDiffLineKind = \"added\" | \"removed\" | \"context\";\n\nexport type AIDiffLine = {\n  content: string;\n  kind: AIDiffLineKind;\n  /** Line number in the file. Omit for tabular data rather than code. */\n  number?: number;\n};\n\nexport type AIDiffProps = {\n  className?: string;\n  lines: AIDiffLine[];\n  onAccept?: () => void;\n  onReject?: () => void;\n  /** File path or a description of what is being changed. */\n  title?: string;\n};\n\nconst PREFIX: Record<AIDiffLineKind, string> = {\n  added: \"+\",\n  context: \" \",\n  removed: \"-\",\n};\n\n/**\n * A proposed edit awaiting a human yes or no.\n *\n * Added lines wipe in from the left with a clip path rather than fading: a wipe\n * has a direction, and direction is what tells you the change was *written*\n * rather than always having been there. Rejecting collapses the lines to zero\n * height so the space is reclaimed — a rejected edit should stop occupying the\n * page.\n */\nconst AIDiff = ({\n  className,\n  lines,\n  onAccept,\n  onReject,\n  title,\n}: AIDiffProps) => {\n  const shouldReduceMotion = useReducedMotion();\n  const [decision, setDecision] = useState<\"accepted\" | \"rejected\" | null>(\n    null\n  );\n\n  const accept = () => {\n    setDecision(\"accepted\");\n    onAccept?.();\n  };\n\n  const reject = () => {\n    setDecision(\"rejected\");\n    onReject?.();\n  };\n\n  const added = lines.filter((line) => line.kind === \"added\").length;\n  const removed = lines.filter((line) => line.kind === \"removed\").length;\n\n  return (\n    <motion.div\n      // One flash on accept, then it settles. Anything more celebratory turns an\n      // ordinary code review into an event.\n      animate={\n        decision && !shouldReduceMotion\n          ? {\n              backgroundColor: [\n                decision === \"accepted\" ? SUCCESS_TINT : DANGER_TINT,\n                \"rgb(0 0 0 / 0)\",\n              ],\n            }\n          : undefined\n      }\n      className={cn(\n        \"w-full overflow-hidden rounded-xl border border-border bg-background\",\n        className\n      )}\n      layout={!shouldReduceMotion}\n      transition={\n        shouldReduceMotion\n          ? { duration: 0 }\n          : {\n              backgroundColor: { duration: FLASH_DURATION, ease: EASE_OUT },\n              layout: SPRING_DEFAULT,\n            }\n      }\n    >\n      <div className=\"flex items-center gap-2 border-border border-b px-3 py-2\">\n        {title ? (\n          <span className=\"min-w-0 truncate font-mono text-foreground text-xs\">\n            {title}\n          </span>\n        ) : null}\n        <span className=\"ml-auto flex shrink-0 items-center gap-1.5 text-xs tabular-nums\">\n          <span className=\"text-[oklch(58%_0.17_150)]\">+{added}</span>\n          <span className=\"text-destructive\">-{removed}</span>\n        </span>\n      </div>\n\n      <AnimatePresence initial={false}>\n        {decision !== \"rejected\" && (\n          <motion.div\n            className=\"overflow-hidden\"\n            exit={\n              shouldReduceMotion\n                ? { opacity: 0, transition: { duration: 0 } }\n                : { height: 0, opacity: 0 }\n            }\n            transition={shouldReduceMotion ? { duration: 0 } : SPRING_DEFAULT}\n          >\n            <pre className=\"overflow-x-auto py-1 font-mono text-xs leading-relaxed\">\n              {lines.map((line, index) => (\n                <motion.div\n                  animate={\n                    shouldReduceMotion\n                      ? undefined\n                      : { clipPath: \"inset(0 0% 0 0)\" }\n                  }\n                  className={cn(\n                    \"flex gap-3 px-3\",\n                    line.kind === \"added\" && \"bg-[oklch(72%_0.17_150_/_0.12)]\",\n                    line.kind === \"removed\" && \"bg-[oklch(63%_0.21_25_/_0.1)]\"\n                  )}\n                  initial={\n                    // Only the added lines wipe. Context lines were already\n                    // there, so animating them would misrepresent the change.\n                    line.kind === \"added\" && !shouldReduceMotion\n                      ? { clipPath: \"inset(0 100% 0 0)\" }\n                      : false\n                  }\n                  // biome-ignore lint/suspicious/noArrayIndexKey: diff lines are positional and may repeat verbatim\n                  key={index}\n                  transition={\n                    shouldReduceMotion\n                      ? { duration: 0 }\n                      : {\n                          delay: index * LINE_STAGGER,\n                          duration: WIPE_DURATION,\n                          ease: EASE_OUT,\n                        }\n                  }\n                >\n                  {line.number !== undefined && (\n                    <span className=\"w-6 shrink-0 select-none text-right text-muted-foreground tabular-nums\">\n                      {line.number}\n                    </span>\n                  )}\n                  <span\n                    className={cn(\n                      \"w-2 shrink-0 select-none\",\n                      line.kind === \"added\" && \"text-[oklch(52%_0.17_150)]\",\n                      line.kind === \"removed\" && \"text-destructive\",\n                      line.kind === \"context\" && \"text-muted-foreground\"\n                    )}\n                  >\n                    {PREFIX[line.kind]}\n                  </span>\n                  <span className=\"whitespace-pre text-foreground\">\n                    {line.content}\n                  </span>\n                </motion.div>\n              ))}\n            </pre>\n          </motion.div>\n        )}\n      </AnimatePresence>\n\n      {(onAccept || onReject) && !decision && (\n        <div className=\"flex items-center justify-end gap-2 border-border border-t px-3 py-2\">\n          {onReject ? (\n            <button\n              className=\"cursor-pointer rounded-lg px-2.5 py-1 text-muted-foreground text-xs transition-colors hover:bg-muted hover:text-foreground\"\n              onClick={reject}\n              type=\"button\"\n            >\n              Reject\n            </button>\n          ) : null}\n          {onAccept ? (\n            <button\n              className=\"cursor-pointer rounded-lg bg-foreground px-2.5 py-1 text-background text-xs\"\n              onClick={accept}\n              type=\"button\"\n            >\n              Accept\n            </button>\n          ) : null}\n        </div>\n      )}\n\n      {decision ? (\n        <motion.p\n          animate={{ opacity: 1 }}\n          className=\"border-border border-t px-3 py-2 text-muted-foreground text-xs capitalize\"\n          initial={shouldReduceMotion ? { opacity: 1 } : { opacity: 0 }}\n          transition={\n            shouldReduceMotion\n              ? { duration: 0 }\n              : { duration: 0.2, ease: EASE_OUT }\n          }\n        >\n          {decision}\n        </motion.p>\n      ) : null}\n    </motion.div>\n  );\n};\n\nexport default AIDiff;\n","path":"index.tsx","target":"components/smoothui/ai-diff/index.tsx","type":"registry:ui"}],"name":"ai-diff","registryDependencies":[],"title":"Ai Diff","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":[],"description":"Deprecated alias for morph-surface. Kept for one release so existing installs keep resolving.","devDependencies":[],"files":[{"content":"\"use client\";\n\n/**\n * @deprecated Renamed to `morph-surface`.\n *\n * This component was never an AI input: its default export has always been\n * `MorphSurface`, a feedback dock that morphs a pill into a panel. The name sent\n * people looking for a chat composer — that is `ai-prompt-input`.\n *\n * This shim keeps `npx shadcn@latest add @smoothui/ai-input` working for one\n * release. Import from `morph-surface` instead:\n *\n * ```tsx\n * import MorphSurface from \"@/components/smoothui/morph-surface\";\n * ```\n */\nexport { default, default as MorphSurface } from \"../morph-surface\";\n","path":"index.tsx","target":"components/smoothui/ai-input/index.tsx","type":"registry:ui"}],"name":"ai-input","registryDependencies":["https://smoothui.dev/r/morph-surface.json"],"title":"Ai Input","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"Family of AI waiting indicators — dots, sweep bar and pixel grid — sharing one cycle length, with an optional elapsed counter.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { motion, useAnimationFrame, useReducedMotion } from \"motion/react\";\nimport { useRef, useState } from \"react\";\n\n/**\n * One cycle length for every variant.\n *\n * Two loaders on the same screen running at different tempos read as two\n * unrelated things loading. Sharing the cycle makes them feel like one system\n * working, which is the whole point of a loader family.\n */\nexport const AI_LOADER_CYCLE_SECONDS = 1.2;\n\nconst DOT_COUNT = 3;\nconst GRID_SIZE = 3;\nconst GRID_CELLS = GRID_SIZE * GRID_SIZE;\n/** Diagonal wave order, so the grid fills like a sweep rather than at random. */\nconst GRID_DELAYS = [0, 1, 2, 1, 2, 3, 2, 3, 4];\nconst EASE_IN_OUT = [0.645, 0.045, 0.355, 1] as const;\nconst MS_PER_SECOND = 1000;\nconst ELAPSED_DECIMALS = 1;\n\nexport type AILoaderVariant = \"dots\" | \"bar\" | \"grid\";\n\nexport type AILoaderProps = {\n  className?: string;\n  /** Optional text before the indicator, e.g. \"Thinking\". */\n  label?: string;\n  /** Appends a live elapsed counter. Long waits need a sign of progress. */\n  showElapsed?: boolean;\n  variant?: AILoaderVariant;\n};\n\nconst Dots = ({ reduced }: { reduced: boolean }) => (\n  <span className=\"flex items-center gap-1\">\n    {Array.from({ length: DOT_COUNT }, (_, index) => (\n      <motion.span\n        animate={reduced ? { opacity: 0.5 } : { opacity: [0.25, 1, 0.25] }}\n        className=\"size-1.5 rounded-full bg-current\"\n        // biome-ignore lint/suspicious/noArrayIndexKey: dots are positional\n        key={index}\n        transition={\n          reduced\n            ? { duration: 0 }\n            : {\n                delay: (index * AI_LOADER_CYCLE_SECONDS) / (DOT_COUNT * 2),\n                duration: AI_LOADER_CYCLE_SECONDS,\n                ease: EASE_IN_OUT,\n                repeat: Number.POSITIVE_INFINITY,\n              }\n        }\n      />\n    ))}\n  </span>\n);\n\nconst Bar = ({ reduced }: { reduced: boolean }) => (\n  <span className=\"relative block h-1 w-24 overflow-hidden rounded-full bg-current/15\">\n    {/* A sweep, not a percentage. Faking determinate progress for an unknown\n        wait is the one thing a loader must never do. */}\n    <motion.span\n      animate={reduced ? { x: \"0%\" } : { x: [\"-100%\", \"200%\"] }}\n      className=\"absolute inset-y-0 w-1/3 rounded-full bg-current\"\n      transition={\n        reduced\n          ? { duration: 0 }\n          : {\n              duration: AI_LOADER_CYCLE_SECONDS * 1.4,\n              ease: EASE_IN_OUT,\n              repeat: Number.POSITIVE_INFINITY,\n            }\n      }\n    />\n  </span>\n);\n\nconst Grid = ({ reduced }: { reduced: boolean }) => (\n  <span className=\"grid grid-cols-3 gap-0.5\">\n    {Array.from({ length: GRID_CELLS }, (_, index) => (\n      <motion.span\n        animate={reduced ? { opacity: 0.45 } : { opacity: [0.2, 1, 0.2] }}\n        className=\"size-1.5 rounded-[2px] bg-current\"\n        // biome-ignore lint/suspicious/noArrayIndexKey: cells are positional\n        key={index}\n        transition={\n          reduced\n            ? { duration: 0 }\n            : {\n                delay:\n                  ((GRID_DELAYS[index] ?? 0) * AI_LOADER_CYCLE_SECONDS) / 8,\n                duration: AI_LOADER_CYCLE_SECONDS,\n                ease: EASE_IN_OUT,\n                repeat: Number.POSITIVE_INFINITY,\n              }\n        }\n      />\n    ))}\n  </span>\n);\n\nconst Elapsed = () => {\n  const startRef = useRef<number | null>(null);\n  const [seconds, setSeconds] = useState(0);\n\n  useAnimationFrame((time) => {\n    const start = startRef.current ?? time;\n    startRef.current = start;\n    const next = (time - start) / MS_PER_SECOND;\n    // Only re-render on a tenth changing — a 60fps counter is unreadable and\n    // re-renders the whole label for nothing.\n    setSeconds((current) =>\n      next.toFixed(ELAPSED_DECIMALS) === current.toFixed(ELAPSED_DECIMALS)\n        ? current\n        : next\n    );\n  });\n\n  return (\n    <span className=\"tabular-nums opacity-60\">\n      {seconds.toFixed(ELAPSED_DECIMALS)}s\n    </span>\n  );\n};\n\n/**\n * The waiting indicator family.\n *\n * All three variants share one cycle length, so a page can mix them without the\n * result looking out of sync. None of them fake determinate progress.\n */\nconst AILoader = ({\n  className,\n  label,\n  showElapsed = false,\n  variant = \"dots\",\n}: AILoaderProps) => {\n  const reduced = Boolean(useReducedMotion());\n\n  return (\n    <span\n      aria-live=\"polite\"\n      className={cn(\n        \"inline-flex items-center gap-2 text-muted-foreground text-sm\",\n        className\n      )}\n      role=\"status\"\n    >\n      {label ? <span>{label}</span> : null}\n      {variant === \"dots\" && <Dots reduced={reduced} />}\n      {variant === \"bar\" && <Bar reduced={reduced} />}\n      {variant === \"grid\" && <Grid reduced={reduced} />}\n      {showElapsed ? <Elapsed /> : null}\n      <span className=\"sr-only\">{label ?? \"Loading\"}</span>\n    </span>\n  );\n};\n\nexport default AILoader;\n","path":"index.tsx","target":"components/smoothui/ai-loader/index.tsx","type":"registry:ui"}],"name":"ai-loader","registryDependencies":[],"title":"Ai Loader","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["lucide-react","motion"],"description":"Chat message bubble whose action row slides out of the bubble's own edge, revealed on hover and on focus.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { Check, Copy, RotateCcw, ThumbsDown, ThumbsUp } from \"lucide-react\";\nimport { type ReactNode, useEffect, useState } from \"react\";\n\nconst COPIED_RESET_MS = 1600;\nconst ACTION_STAGGER_MS = 30;\n\n/**\n * The reveal is CSS so it needs no state and cannot desync from the pointer.\n * `motion`'s `animate` target was not being re-applied on state change here, and\n * a hover fade does not need a spring — a 200ms ease-out is the whole effect.\n */\nconst ACTION_STYLES = `\n.ai-message-action {\n  opacity: 0;\n  transform: translateX(var(--ai-message-slide)) scale(0.9);\n  transition:\n    opacity 200ms cubic-bezier(.23, 1, .32, 1),\n    transform 200ms cubic-bezier(.23, 1, .32, 1),\n    background-color 150ms ease,\n    color 150ms ease;\n}\n.ai-message-action-agent { --ai-message-slide: -6px; }\n.ai-message-action-user { --ai-message-slide: 6px; }\n.ai-message-root:hover .ai-message-action,\n.ai-message-root:focus-within .ai-message-action {\n  opacity: 1;\n  transform: translateX(0) scale(1);\n}\n.ai-message-pop { animation: ai-message-pop 250ms cubic-bezier(.23, 1, .32, 1); }\n@keyframes ai-message-pop {\n  0% { transform: scale(1); }\n  45% { transform: scale(1.25); }\n  100% { transform: scale(1); }\n}\n@media (prefers-reduced-motion: reduce) {\n  .ai-message-action { transition-duration: 0ms; transition-delay: 0ms !important; transform: none; }\n  .ai-message-root:hover .ai-message-action,\n  .ai-message-root:focus-within .ai-message-action { transform: none; }\n  .ai-message-pop { animation: none; }\n}\n`;\n\nexport type AIMessageAuthor = \"user\" | \"assistant\";\n\nexport type AIMessageProps = {\n  /** Rendered to the side of the bubble — an avatar or an orb. */\n  avatar?: ReactNode;\n  /**\n   * Draw the tinted bubble. Turn it off for assistant turns that carry their own\n   * surfaces — reasoning traces, tool calls, diffs — where a bubble around a\n   * stack of cards reads as a box inside a box.\n   */\n  bubble?: boolean;\n  children: ReactNode;\n  className?: string;\n  /** Plain text handed to the clipboard. Omit to hide the copy action. */\n  copyText?: string;\n  /**\n   * Who wrote it. Named `from` rather than `role` on purpose: `role` is an ARIA\n   * attribute, and a component prop of that name misleads both readers and\n   * accessibility linters.\n   */\n  from?: AIMessageAuthor;\n  onRetry?: () => void;\n  onVote?: (vote: \"up\" | \"down\") => void;\n  /** Preformatted timestamp, e.g. \"14:32\". */\n  timestamp?: string;\n};\n\n/**\n * A chat message with actions that stay out of the way.\n *\n * The action row slides out of the bubble's own edge rather than fading in from\n * nowhere, so it reads as belonging to that message. It is revealed on hover and\n * on focus-within, because a hover-only control row is unreachable by keyboard.\n */\nconst AIMessage = ({\n  avatar,\n  bubble = true,\n  children,\n  className,\n  copyText,\n  onRetry,\n  onVote,\n  from = \"assistant\",\n  timestamp,\n}: AIMessageProps) => {\n  const [hasCopied, setHasCopied] = useState(false);\n  const [vote, setVote] = useState<\"up\" | \"down\" | null>(null);\n\n  const isUser = from === \"user\";\n\n  useEffect(() => {\n    if (!hasCopied) {\n      return;\n    }\n    const timeout = setTimeout(() => setHasCopied(false), COPIED_RESET_MS);\n    return () => clearTimeout(timeout);\n  }, [hasCopied]);\n\n  const copy = async () => {\n    if (!copyText) {\n      return;\n    }\n    try {\n      await navigator.clipboard.writeText(copyText);\n      setHasCopied(true);\n    } catch {\n      // A blocked clipboard is not worth interrupting the conversation over.\n    }\n  };\n\n  const actions = [\n    copyText\n      ? {\n          active: hasCopied,\n          icon: hasCopied ? Check : Copy,\n          key: \"copy\",\n          label: hasCopied ? \"Copied\" : \"Copy\",\n          onClick: copy,\n        }\n      : null,\n    onRetry\n      ? {\n          active: false,\n          icon: RotateCcw,\n          key: \"retry\",\n          label: \"Retry\",\n          onClick: onRetry,\n        }\n      : null,\n    // Voting on your own message makes no sense, so the feedback pair is\n    // assistant-only even when the consumer passes `onVote` for the thread.\n    onVote && !isUser\n      ? {\n          active: vote === \"up\",\n          icon: ThumbsUp,\n          key: \"up\",\n          label: \"Good response\",\n          onClick: () => {\n            setVote(\"up\");\n            onVote(\"up\");\n          },\n        }\n      : null,\n    onVote && !isUser\n      ? {\n          active: vote === \"down\",\n          icon: ThumbsDown,\n          key: \"down\",\n          label: \"Bad response\",\n          onClick: () => {\n            setVote(\"down\");\n            onVote(\"down\");\n          },\n        }\n      : null,\n  ].filter((action): action is NonNullable<typeof action> => action !== null);\n\n  return (\n    <div\n      className={cn(\n        // The reveal is scoped to this class rather than Tailwind's `group`, so a\n        // `group` ancestor elsewhere on the page cannot reveal every row at once.\n        \"ai-message-root flex w-full gap-2.5\",\n        isUser ? \"flex-row-reverse\" : \"flex-row\",\n        className\n      )}\n    >\n      {/* biome-ignore lint/security/noDangerouslySetInnerHtml: a static, local stylesheet with no interpolation */}\n      <style dangerouslySetInnerHTML={{ __html: ACTION_STYLES }} />\n\n      {avatar ? <div className=\"mt-0.5 shrink-0\">{avatar}</div> : null}\n\n      <div className={cn(\"flex min-w-0 flex-col gap-1\", isUser && \"items-end\")}>\n        <div\n          className={cn(\n            \"w-fit max-w-prose text-sm leading-relaxed\",\n            bubble && \"rounded-2xl px-3.5 py-2.5\",\n            bubble && isUser && \"rounded-br-md bg-foreground text-background\",\n            bubble && !isUser && \"rounded-bl-md bg-muted text-foreground\",\n            !bubble && \"text-foreground\"\n          )}\n        >\n          {children}\n        </div>\n\n        <div\n          className={cn(\n            \"flex items-center gap-1 px-1\",\n            isUser ? \"flex-row-reverse\" : \"flex-row\"\n          )}\n        >\n          {/* The timestamp comes first so it stays pinned to the edge the\n              bubble is anchored to — left for the assistant, right for the\n              user. Putting the (always-mounted, invisible) action slots before\n              it pushed it toward the middle of the row, where it read as\n              floating in nothing. */}\n          {timestamp ? (\n            <span className=\"text-muted-foreground text-xs tabular-nums\">\n              {timestamp}\n            </span>\n          ) : null}\n\n          {/* Always mounted, only faded — mounting the row on hover changed its\n              height, so every message below jumped as the pointer moved down a\n              thread. The reveal is plain CSS rather than a motion `animate`\n              target: the group already knows about hover and focus-within, so no\n              state, no listeners, and the row cannot get stuck half-revealed. */}\n          {actions.map((action, index) => {\n            const Icon = action.icon;\n            return (\n              <button\n                aria-label={action.label}\n                aria-pressed={action.active}\n                className={cn(\n                  \"ai-message-action cursor-pointer rounded-lg p-1.5\",\n                  isUser ? \"ai-message-action-user\" : \"ai-message-action-agent\",\n                  action.active\n                    ? \"text-foreground\"\n                    : \"text-muted-foreground hover:bg-muted hover:text-foreground\"\n                )}\n                key={action.key}\n                onClick={action.onClick}\n                style={{ transitionDelay: `${index * ACTION_STAGGER_MS}ms` }}\n                type=\"button\"\n              >\n                <Icon\n                  aria-hidden=\"true\"\n                  className={\n                    action.key === \"copy\" && hasCopied\n                      ? \"ai-message-pop\"\n                      : undefined\n                  }\n                  key={action.key === \"copy\" && hasCopied ? \"copied\" : \"idle\"}\n                  size={14}\n                />\n              </button>\n            );\n          })}\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default AIMessage;\n","path":"index.tsx","target":"components/smoothui/ai-message/index.tsx","type":"registry:ui"}],"name":"ai-message","registryDependencies":[],"title":"Ai Message","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"Cartoon character orb whose expression carries the AI state — cursor-following gaze, natural blinks, thinking saccades, happy arcs and spiral eyes.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport {\n  motion,\n  useAnimationControls,\n  useReducedMotion,\n  useSpring,\n} from \"motion/react\";\nimport { useCallback, useEffect, useId, useRef } from \"react\";\nimport {\n  type AIAmplitude,\n  type AIState,\n  getAIStateMotion,\n  useAmplitudeValue,\n} from \"../ai-core\";\n\nconst VIEWBOX = 100;\nconst CENTER = VIEWBOX / 2;\nconst EYE_OFFSET = 16;\nconst EYE_Y = 44;\nconst EYE_WIDTH = 11;\nconst EYE_HEIGHT = 26;\nconst EYE_RADIUS = 5.5;\n/** How far the pupils can travel from centre, in viewBox units. */\nconst GAZE_RANGE = 5.5;\n/** Cursor distance, in px, at which the gaze reaches full deflection. */\nconst GAZE_FALLOFF = 220;\nconst EASE_OUT = [0.23, 1, 0.32, 1] as const;\nconst EASE_IN = [0.4, 0, 1, 1] as const;\n/** A ~1.25-turn swirl; spun in place it reads as dizzy. */\nconst SPIRAL = \"M0 0C-0.6 -4 5 -5 6 -0.6C7 4.5 1 8 -4 6C-9 4.5 -9.5 -2 -6 -6\";\nconst BLINK_MIN_MS = 3200;\nconst BLINK_EXTRA_MS = 2600;\nconst DOUBLE_BLINK_CHANCE = 0.25;\n/** Thinking saccades: the eyes look away and up, the way people search. */\nconst SACCADE_MIN_MS = 700;\nconst SACCADE_EXTRA_MS = 700;\nconst SACCADE_TARGETS = [\n  { x: -1, y: -1 },\n  { x: 1, y: -1 },\n  { x: -0.6, y: -0.4 },\n  { x: 0.8, y: -0.9 },\n] as const;\n\nexport type AIOrbFaceProps = {\n  /** Accessible label. Omit to keep the character decorative. */\n  \"aria-label\"?: string;\n  /** Live audio level, 0–1. Widens the eyes and lifts the body while speaking. */\n  amplitude?: AIAmplitude;\n  className?: string;\n  colors?: { body?: string; bodyEdge?: string; feature?: string };\n  /**\n   * Follow the pointer with its gaze. Turn off inside dense UI where a dozen\n   * of these tracking the cursor would be noise.\n   */\n  gaze?: boolean;\n  /** Rendered size. Numbers are pixels. */\n  size?: number | string;\n  state?: AIState;\n};\n\nconst DEFAULT_COLORS = {\n  body: \"oklch(78% 0.14 280)\",\n  bodyEdge: \"oklch(70% 0.16 320)\",\n  feature: \"oklch(24% 0.03 280)\",\n};\n\n/**\n * A little character rather than an indicator.\n *\n * Status is carried by expression — the thing humans read fastest and the reason\n * a literal checkmark stuck onto an orb feels wrong. Squints while it thinks,\n * eyes wide while it listens, happy arcs when it finishes, spiral-eyed when it\n * breaks. The gaze and blink cadence are borrowed from the SmoothUI moai so the\n * two feel like the same creature.\n */\nconst AIOrbFace = ({\n  \"aria-label\": ariaLabel,\n  amplitude,\n  className,\n  colors,\n  gaze = true,\n  size = 128,\n  state = \"idle\",\n}: AIOrbFaceProps) => {\n  const shouldReduceMotion = useReducedMotion();\n  const amplitudeValue = useAmplitudeValue(amplitude);\n  const stateMotion = getAIStateMotion(state);\n  const finalColors = { ...DEFAULT_COLORS, ...colors };\n\n  // Gradient ids must be unique per instance, or a second face on the page\n  // silently repaints the first one's body.\n  const bodyGradientId = `${useId()}-body`;\n  const svgRef = useRef<SVGSVGElement | null>(null);\n  const leftLid = useAnimationControls();\n  const rightLid = useAnimationControls();\n  const bodyControls = useAnimationControls();\n\n  const gazeX = useSpring(0, { damping: 26, stiffness: 220 });\n  const gazeY = useSpring(0, { damping: 26, stiffness: 220 });\n\n  const resolvedSize = typeof size === \"number\" ? `${size}px` : size;\n\n  const isHappy = state === \"done\";\n  const isThinking = state === \"thinking\";\n  const isListening = state === \"listening\";\n  const isBroken = state === \"error\";\n  const eyesOpen = !(isHappy || isBroken);\n\n  // How open the eyes rest for this state.\n  //\n  // This drives the rect's `height`, never its `scaleY`. The blink owns scaleY\n  // outright — when both the squint and the blink wrote to scaleY they fought,\n  // and the eyes ended up stuck at whichever animation finished last.\n  const openScale = (() => {\n    if (isListening) {\n      return 1.15;\n    }\n    if (isThinking) {\n      return 0.62;\n    }\n    if (state === \"streaming\") {\n      return 0.88;\n    }\n    return 1;\n  })();\n\n  // A blink spans several awaits, so the component can unmount mid-blink — a\n  // route change, a state that hides the eyes. Animation controls throw if they\n  // are driven after unmount, so every leg checks first.\n  const mountedRef = useRef(false);\n  useEffect(() => {\n    mountedRef.current = true;\n    return () => {\n      mountedRef.current = false;\n    };\n  }, []);\n\n  // Snap shut, ease back open — an even-timed blink reads as a machine.\n  const blink = useCallback(\n    async (double = false) => {\n      const closeT = { duration: 0.07, ease: EASE_IN };\n      const openT = { duration: 0.16, ease: EASE_OUT };\n      if (!mountedRef.current) {\n        return;\n      }\n      await Promise.all([\n        leftLid.start({ scaleY: 0.08 }, closeT),\n        rightLid.start({ scaleY: 0.08 }, closeT),\n      ]);\n      if (!mountedRef.current) {\n        return;\n      }\n      await Promise.all([\n        leftLid.start({ scaleY: 1 }, double ? closeT : openT),\n        rightLid.start({ scaleY: 1 }, double ? closeT : openT),\n      ]);\n      if (double) {\n        await blink(false);\n      }\n    },\n    [leftLid, rightLid]\n  );\n\n  // Idle blinking, occasionally a double.\n  useEffect(() => {\n    if (shouldReduceMotion || !eyesOpen) {\n      return;\n    }\n    let timeout: ReturnType<typeof setTimeout>;\n    let mounted = true;\n    const schedule = () => {\n      timeout = setTimeout(\n        () => {\n          if (mounted) {\n            blink(Math.random() < DOUBLE_BLINK_CHANCE);\n          }\n          schedule();\n        },\n        BLINK_MIN_MS + Math.random() * BLINK_EXTRA_MS\n      );\n    };\n    schedule();\n    return () => {\n      mounted = false;\n      clearTimeout(timeout);\n    };\n  }, [blink, eyesOpen, shouldReduceMotion]);\n\n  // Gaze follows the pointer, except while thinking — then it looks away, which\n  // is exactly what makes \"thinking\" legible without any added graphic.\n  useEffect(() => {\n    if (!gaze || shouldReduceMotion || isThinking || !eyesOpen) {\n      return;\n    }\n    const handle = (event: PointerEvent) => {\n      const svg = svgRef.current;\n      if (!svg) {\n        return;\n      }\n      const rect = svg.getBoundingClientRect();\n      const dx = event.clientX - (rect.left + rect.width / 2);\n      const dy = event.clientY - (rect.top + rect.height / 2);\n      const distance = Math.sqrt(dx * dx + dy * dy);\n      const reach = Math.min(1, distance / GAZE_FALLOFF) * GAZE_RANGE;\n      const angle = Math.atan2(dy, dx);\n      gazeX.set(Math.cos(angle) * reach);\n      gazeY.set(Math.sin(angle) * reach);\n    };\n    window.addEventListener(\"pointermove\", handle);\n    return () => window.removeEventListener(\"pointermove\", handle);\n  }, [gaze, gazeX, gazeY, isThinking, eyesOpen, shouldReduceMotion]);\n\n  // Thinking saccades.\n  useEffect(() => {\n    if (!isThinking || shouldReduceMotion) {\n      return;\n    }\n    let timeout: ReturnType<typeof setTimeout>;\n    let mounted = true;\n    let index = 0;\n    const schedule = () => {\n      timeout = setTimeout(\n        () => {\n          if (!mounted) {\n            return;\n          }\n          const target = SACCADE_TARGETS[index % SACCADE_TARGETS.length];\n          index += 1;\n          gazeX.set(target.x * GAZE_RANGE);\n          gazeY.set(target.y * GAZE_RANGE);\n          schedule();\n        },\n        SACCADE_MIN_MS + Math.random() * SACCADE_EXTRA_MS\n      );\n    };\n    schedule();\n    return () => {\n      mounted = false;\n      clearTimeout(timeout);\n    };\n  }, [gazeX, gazeY, isThinking, shouldReduceMotion]);\n\n  // Error: one dizzy wobble.\n  useEffect(() => {\n    if (!isBroken) {\n      return;\n    }\n    gazeX.set(0);\n    gazeY.set(0);\n    if (shouldReduceMotion) {\n      return;\n    }\n    // The wobble plays once, but the spiral eyes stay for as long as the state\n    // is `error`. Recovering to a neutral face after a couple of seconds would\n    // leave a broken assistant looking fine.\n    bodyControls.start(\n      { rotate: [0, -11, 9, -7, 5, -3, 0], x: [0, -5, 4, -3, 2, -1, 0] },\n      { duration: 1.05, ease: [0.45, 0, 0.55, 1] }\n    );\n  }, [bodyControls, gazeX, gazeY, isBroken, shouldReduceMotion]);\n\n  // Done: a happy hop. The squash-and-stretch is the whole payload.\n  useEffect(() => {\n    if (!isHappy || shouldReduceMotion) {\n      return;\n    }\n    bodyControls.start(\n      {\n        scaleX: [1, 1.08, 0.94, 1.04, 0.99, 1],\n        scaleY: [1, 0.9, 1.08, 0.95, 1.02, 1],\n        y: [0, 3, -9, 0, -3, 0],\n      },\n      { duration: 0.85, ease: EASE_OUT, times: [0, 0.12, 0.4, 0.62, 0.82, 1] }\n    );\n  }, [bodyControls, isHappy, shouldReduceMotion]);\n\n  // Listening: the body breathes with the voice. Driven from the MotionValue so\n  // a 60fps audio signal never re-renders this component.\n  useEffect(() => {\n    if (shouldReduceMotion || !isListening) {\n      return;\n    }\n    const unsubscribe = amplitudeValue.on(\"change\", (level) => {\n      bodyControls.set({ scale: 1 + level * 0.07, y: -level * 2 });\n    });\n    return unsubscribe;\n  }, [amplitudeValue, bodyControls, isListening, shouldReduceMotion]);\n\n  // Reset the breathing here rather than in the effect above. Animation\n  // controls reject `set()` after unmount, and an unsubscribe cleanup is\n  // exactly when the component may already be gone.\n  useEffect(() => {\n    if (!(isListening || !mountedRef.current)) {\n      bodyControls.set({ scale: 1, y: 0 });\n    }\n  }, [bodyControls, isListening]);\n\n  const renderEye = (side: -1 | 1) => {\n    const x = CENTER + side * EYE_OFFSET - EYE_WIDTH / 2;\n    const controls = side === -1 ? leftLid : rightLid;\n    const height = EYE_HEIGHT * openScale;\n    // Keep the eye centred as it opens and closes, so a squint reads as lids\n    // meeting rather than the eye sliding up the face.\n    const y = EYE_Y + (EYE_HEIGHT - height) / 2;\n\n    return (\n      <motion.rect\n        animate={controls}\n        fill={finalColors.feature}\n        height={height}\n        initial={{ scaleY: 1 }}\n        rx={Math.min(EYE_RADIUS, height / 2)}\n        style={{\n          transformOrigin: `${x + EYE_WIDTH / 2}px ${y + height / 2}px`,\n          x: gazeX,\n          y: gazeY,\n        }}\n        transition={\n          shouldReduceMotion\n            ? { duration: 0 }\n            : { bounce: 0.1, duration: 0.25, type: \"spring\" }\n        }\n        width={EYE_WIDTH}\n        x={x}\n        y={y}\n      />\n    );\n  };\n\n  const renderHappyEye = (side: -1 | 1) => {\n    const cx = CENTER + side * EYE_OFFSET;\n    const y = EYE_Y + EYE_HEIGHT / 2;\n    return (\n      <motion.path\n        animate={{ pathLength: 1 }}\n        d={`M${cx - 8} ${y + 3} Q${cx} ${y - 11} ${cx + 8} ${y + 3}`}\n        fill=\"none\"\n        initial={shouldReduceMotion ? { pathLength: 1 } : { pathLength: 0 }}\n        stroke={finalColors.feature}\n        strokeLinecap=\"round\"\n        strokeWidth={7}\n        transition={\n          shouldReduceMotion\n            ? { duration: 0 }\n            : { delay: side === -1 ? 0 : 0.06, duration: 0.3, ease: EASE_OUT }\n        }\n      />\n    );\n  };\n\n  const renderDizzyEye = (side: -1 | 1) => (\n    <motion.path\n      animate={shouldReduceMotion ? undefined : { rotate: 360 * side }}\n      d={SPIRAL}\n      fill=\"none\"\n      stroke={finalColors.feature}\n      strokeLinecap=\"round\"\n      strokeWidth={3}\n      style={{\n        scale: 1.5,\n        x: CENTER + side * EYE_OFFSET,\n        y: EYE_Y + EYE_HEIGHT / 2,\n      }}\n      transition={{\n        duration: 2.4,\n        ease: \"linear\",\n        repeat: Number.POSITIVE_INFINITY,\n      }}\n    />\n  );\n\n  const renderMouth = () => {\n    if (isHappy) {\n      return (\n        <motion.path\n          animate={{ pathLength: 1 }}\n          d={`M${CENTER - 11} 76 Q${CENTER} 88 ${CENTER + 11} 76`}\n          fill=\"none\"\n          initial={shouldReduceMotion ? { pathLength: 1 } : { pathLength: 0 }}\n          stroke={finalColors.feature}\n          strokeLinecap=\"round\"\n          strokeWidth={5}\n          transition={\n            shouldReduceMotion\n              ? { duration: 0 }\n              : { duration: 0.32, ease: EASE_OUT }\n          }\n        />\n      );\n    }\n    if (isBroken) {\n      // A woozy wave, not a frown — it is confused, not scolding the user.\n      return (\n        <path\n          d={`M${CENTER - 12} 79 q6 -7 12 0 t12 0`}\n          fill=\"none\"\n          stroke={finalColors.feature}\n          strokeLinecap=\"round\"\n          strokeWidth={4}\n        />\n      );\n    }\n    if (isThinking) {\n      // Off-centre line: the universal \"hmm\".\n      return (\n        <motion.line\n          animate={{ x: [0, 3, 0] }}\n          stroke={finalColors.feature}\n          strokeLinecap=\"round\"\n          strokeWidth={4}\n          transition={\n            shouldReduceMotion\n              ? { duration: 0 }\n              : {\n                  duration: 2.4,\n                  ease: EASE_OUT,\n                  repeat: Number.POSITIVE_INFINITY,\n                }\n          }\n          x1={CENTER - 6}\n          x2={CENTER + 8}\n          y1={78}\n          y2={78}\n        />\n      );\n    }\n    return (\n      <line\n        stroke={finalColors.feature}\n        strokeLinecap=\"round\"\n        strokeWidth={4}\n        x1={CENTER - 7}\n        x2={CENTER + 7}\n        y1={78}\n        y2={78}\n      />\n    );\n  };\n\n  return (\n    <motion.svg\n      animate={bodyControls}\n      aria-hidden={ariaLabel ? undefined : true}\n      aria-label={ariaLabel}\n      className={cn(\"block overflow-visible\", className)}\n      ref={svgRef}\n      role={ariaLabel ? \"img\" : undefined}\n      style={{\n        filter: `saturate(${stateMotion.saturation})`,\n        height: resolvedSize,\n        width: resolvedSize,\n      }}\n      viewBox={`0 0 ${VIEWBOX} ${VIEWBOX}`}\n    >\n      <title>{ariaLabel ?? \"AI assistant character\"}</title>\n      <defs>\n        <radialGradient cx=\"35%\" cy=\"28%\" id={bodyGradientId} r=\"80%\">\n          <stop offset=\"0%\" stopColor={finalColors.body} />\n          <stop offset=\"100%\" stopColor={finalColors.bodyEdge} />\n        </radialGradient>\n      </defs>\n\n      <circle cx={CENTER} cy={CENTER} fill={`url(#${bodyGradientId})`} r={48} />\n      {/* One highlight is enough to make it a body rather than a flat disc. */}\n      <ellipse\n        cx={CENTER - 14}\n        cy={CENTER - 22}\n        fill=\"rgb(255 255 255 / 0.4)\"\n        rx={13}\n        ry={8}\n      />\n\n      {eyesOpen && renderEye(-1)}\n      {eyesOpen && renderEye(1)}\n      {isHappy && renderHappyEye(-1)}\n      {isHappy && renderHappyEye(1)}\n      {isBroken && renderDizzyEye(-1)}\n      {isBroken && renderDizzyEye(1)}\n      {renderMouth()}\n    </motion.svg>\n  );\n};\n\nexport default AIOrbFace;\n","path":"index.tsx","target":"components/smoothui/ai-orb-face/index.tsx","type":"registry:ui"}],"name":"ai-orb-face","registryDependencies":["https://smoothui.dev/r/ai-core.json"],"title":"Ai Orb Face","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["lucide-react","motion"],"description":"AI prompt composer with autogrowing textarea, attachment chips and a send-to-stop path morph.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { ArrowUp, Paperclip, Square, X } from \"lucide-react\";\nimport {\n  AnimatePresence,\n  type MotionStyle,\n  motion,\n  type Transition,\n  useReducedMotion,\n} from \"motion/react\";\nimport {\n  type ChangeEvent,\n  type KeyboardEvent,\n  type ReactNode,\n  useCallback,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport {\n  type AIState,\n  getAIStateAccentColor,\n  getAIStateMotion,\n} from \"../ai-core\";\n\nconst SUBMIT_ICON_SIZE = 17;\nconst STOP_ICON_SIZE = 13;\n\nconst MIN_ROWS = 1;\nconst MAX_ROWS = 8;\nconst COUNTER_VISIBLE_RATIO = 0.8;\nconst SPRING_DEFAULT: Transition = {\n  bounce: 0.1,\n  duration: 0.25,\n  type: \"spring\",\n};\nconst SPRING_SNAPPY: Transition = {\n  bounce: 0,\n  duration: 0.2,\n  type: \"spring\",\n};\nconst EASE_OUT = [0.23, 1, 0.32, 1] as const;\nconst ATTACHMENT_STAGGER = 0.035;\n\nexport type AIPromptAttachment = {\n  id: string;\n  /** Bytes. Rendered as a human-readable size when present. */\n  size?: number;\n  name: string;\n};\n\nexport type AIPromptInputProps = {\n  /** Files already attached to the draft. */\n  attachments?: AIPromptAttachment[];\n  /** Extra controls rendered on the left of the toolbar — model pickers etc. */\n  children?: ReactNode;\n  className?: string;\n  disabled?: boolean;\n  /** Shows a counter once the draft passes 80% of the limit. */\n  maxLength?: number;\n  onAttach?: () => void;\n  onRemoveAttachment?: (id: string) => void;\n  /** Called when the submit control is pressed while `state` is `streaming`. */\n  onStop?: () => void;\n  onSubmit?: (value: string) => void;\n  onValueChange?: (value: string) => void;\n  placeholder?: string;\n  /** Shared AI state. `streaming` turns submit into stop. */\n  state?: AIState;\n  /** Controlled draft. Leave undefined to let the component own it. */\n  value?: string;\n};\n\nconst formatSize = (bytes: number): string => {\n  const kilobyte = 1024;\n  if (bytes < kilobyte) {\n    return `${bytes} B`;\n  }\n  const megabyte = kilobyte * kilobyte;\n  if (bytes < megabyte) {\n    return `${Math.round(bytes / kilobyte)} KB`;\n  }\n  return `${(bytes / megabyte).toFixed(1)} MB`;\n};\n\n/**\n * The prompt composer.\n *\n * Growth is animated with a layout spring rather than a CSS height transition,\n * so a pasted paragraph settles instead of snapping — and because the height is\n * measured from the textarea's own scroll height, the surrounding page never\n * reflows mid-keystroke.\n */\nconst AIPromptInput = ({\n  attachments = [],\n  children,\n  className,\n  disabled = false,\n  maxLength,\n  onAttach,\n  onRemoveAttachment,\n  onStop,\n  onSubmit,\n  onValueChange,\n  placeholder = \"Ask anything…\",\n  state = \"idle\",\n  value,\n}: AIPromptInputProps) => {\n  const shouldReduceMotion = useReducedMotion();\n  const stateMotion = getAIStateMotion(state);\n  const accentColor = getAIStateAccentColor(state, \"transparent\");\n\n  const textareaRef = useRef<HTMLTextAreaElement | null>(null);\n  const [internalValue, setInternalValue] = useState(\"\");\n  const [isFocused, setIsFocused] = useState(false);\n\n  const isControlled = value !== undefined;\n  const draft = isControlled ? value : internalValue;\n  const isStreaming = state === \"streaming\";\n  const canSubmit = draft.trim().length > 0 && !disabled;\n\n  const resize = useCallback(() => {\n    const textarea = textareaRef.current;\n    if (!textarea) {\n      return;\n    }\n    // Collapse first, otherwise scrollHeight only ever grows.\n    textarea.style.height = \"auto\";\n    const lineHeight = Number.parseFloat(\n      getComputedStyle(textarea).lineHeight || \"20\"\n    );\n    const maxHeight = lineHeight * MAX_ROWS;\n    textarea.style.height = `${Math.min(textarea.scrollHeight, maxHeight)}px`;\n    textarea.style.overflowY =\n      textarea.scrollHeight > maxHeight ? \"auto\" : \"hidden\";\n  }, []);\n\n  useLayoutEffect(() => {\n    // Collapsing on an empty draft matters after submit: the box has to return\n    // to one row instead of holding the height of the message just sent.\n    if (draft.length === 0 && textareaRef.current) {\n      textareaRef.current.style.height = \"\";\n    }\n    resize();\n  }, [draft, resize]);\n\n  const handleChange = (event: ChangeEvent<HTMLTextAreaElement>) => {\n    const next = event.target.value;\n    if (!isControlled) {\n      setInternalValue(next);\n    }\n    onValueChange?.(next);\n  };\n\n  const submit = () => {\n    if (isStreaming) {\n      onStop?.();\n      return;\n    }\n    if (!canSubmit) {\n      return;\n    }\n    onSubmit?.(draft.trim());\n    if (!isControlled) {\n      setInternalValue(\"\");\n    }\n  };\n\n  const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {\n    // Enter sends, Shift+Enter breaks the line — the convention every chat UI\n    // has trained users on.\n    if (event.key === \"Enter\" && !event.shiftKey) {\n      event.preventDefault();\n      submit();\n    }\n  };\n\n  const counterVisible =\n    maxLength !== undefined &&\n    draft.length >= maxLength * COUNTER_VISIBLE_RATIO;\n  const overLimit = maxLength !== undefined && draft.length > maxLength;\n\n  return (\n    <motion.div\n      className={cn(\n        \"relative w-full rounded-2xl border bg-background\",\n        isFocused ? \"border-foreground/40\" : \"border-border\",\n        disabled && \"opacity-60\",\n        className\n      )}\n      layout={shouldReduceMotion ? false : \"position\"}\n      style={\n        {\n          // The accent ring is the only place status shows here: a composer that\n          // changes shape per state would fight the text the user is writing.\n          boxShadow:\n            state === \"error\" || state === \"done\"\n              ? `0 0 0 1px ${accentColor}`\n              : undefined,\n        } as MotionStyle\n      }\n      transition={shouldReduceMotion ? { duration: 0 } : SPRING_DEFAULT}\n    >\n      <AnimatePresence initial={false}>\n        {attachments.length > 0 && (\n          <motion.ul\n            animate={{ height: \"auto\", opacity: 1 }}\n            className=\"flex list-none flex-wrap gap-2 overflow-hidden px-3 pt-3\"\n            exit={\n              shouldReduceMotion\n                ? { opacity: 0, transition: { duration: 0 } }\n                : { height: 0, opacity: 0 }\n            }\n            initial={\n              shouldReduceMotion\n                ? { height: \"auto\", opacity: 1 }\n                : { height: 0, opacity: 0 }\n            }\n            transition={shouldReduceMotion ? { duration: 0 } : SPRING_DEFAULT}\n          >\n            {attachments.map((attachment, index) => (\n              <motion.li\n                animate={{ opacity: 1, scale: 1, y: 0 }}\n                className=\"flex items-center gap-1.5 rounded-lg border border-border bg-muted/50 py-1 pr-1 pl-2 text-xs\"\n                exit={\n                  shouldReduceMotion\n                    ? { opacity: 0, transition: { duration: 0 } }\n                    : { opacity: 0, scale: 0.9 }\n                }\n                initial={\n                  shouldReduceMotion\n                    ? { opacity: 1, scale: 1, y: 0 }\n                    : { opacity: 0, scale: 0.9, y: 4 }\n                }\n                key={attachment.id}\n                layout={!shouldReduceMotion}\n                transition={\n                  shouldReduceMotion\n                    ? { duration: 0 }\n                    : { ...SPRING_DEFAULT, delay: index * ATTACHMENT_STAGGER }\n                }\n              >\n                <span className=\"max-w-40 truncate\">{attachment.name}</span>\n                {attachment.size !== undefined && (\n                  <span className=\"text-muted-foreground\">\n                    {formatSize(attachment.size)}\n                  </span>\n                )}\n                {onRemoveAttachment ? (\n                  <button\n                    aria-label={`Remove ${attachment.name}`}\n                    className=\"cursor-pointer rounded-md p-0.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground\"\n                    onClick={() => onRemoveAttachment(attachment.id)}\n                    type=\"button\"\n                  >\n                    <X aria-hidden=\"true\" size={12} />\n                  </button>\n                ) : null}\n              </motion.li>\n            ))}\n          </motion.ul>\n        )}\n      </AnimatePresence>\n\n      <textarea\n        className=\"max-h-64 w-full resize-none bg-transparent px-4 pt-3 pb-2 text-sm outline-none placeholder:text-muted-foreground\"\n        disabled={disabled}\n        onBlur={() => setIsFocused(false)}\n        onChange={handleChange}\n        onFocus={() => setIsFocused(true)}\n        onKeyDown={handleKeyDown}\n        placeholder={placeholder}\n        ref={textareaRef}\n        rows={MIN_ROWS}\n        value={draft}\n      />\n\n      <div className=\"flex items-center justify-between gap-2 px-2 pb-2\">\n        <div className=\"flex min-w-0 items-center gap-1\">\n          {onAttach ? (\n            <button\n              aria-label=\"Attach a file\"\n              className=\"cursor-pointer rounded-lg p-2 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground\"\n              disabled={disabled}\n              onClick={onAttach}\n              type=\"button\"\n            >\n              <Paperclip aria-hidden=\"true\" size={16} />\n            </button>\n          ) : null}\n          {children}\n        </div>\n\n        <div className=\"flex items-center gap-2\">\n          <AnimatePresence initial={false}>\n            {counterVisible ? (\n              <motion.span\n                animate={{ opacity: 1, y: 0 }}\n                className={cn(\n                  \"text-xs tabular-nums\",\n                  overLimit ? \"text-destructive\" : \"text-muted-foreground\"\n                )}\n                exit={\n                  shouldReduceMotion\n                    ? { opacity: 0, transition: { duration: 0 } }\n                    : { opacity: 0, y: 4 }\n                }\n                initial={\n                  shouldReduceMotion\n                    ? { opacity: 1, y: 0 }\n                    : { opacity: 0, y: 4 }\n                }\n                transition={\n                  shouldReduceMotion ? { duration: 0 } : SPRING_SNAPPY\n                }\n              >\n                {draft.length}/{maxLength}\n              </motion.span>\n            ) : null}\n          </AnimatePresence>\n\n          <AIPromptSubmit\n            disabled={disabled || !(canSubmit || isStreaming)}\n            isStreaming={isStreaming}\n            onClick={submit}\n            shouldReduceMotion={Boolean(shouldReduceMotion)}\n            speed={stateMotion.speed}\n          />\n        </div>\n      </div>\n    </motion.div>\n  );\n};\n\ntype AIPromptSubmitProps = {\n  disabled: boolean;\n  isStreaming: boolean;\n  onClick: () => void;\n  shouldReduceMotion: boolean;\n  speed: number;\n};\n\nconst AIPromptSubmit = ({\n  disabled,\n  isStreaming,\n  onClick,\n  shouldReduceMotion,\n  speed,\n}: AIPromptSubmitProps) => (\n  <motion.button\n    aria-label={isStreaming ? \"Stop generating\" : \"Send message\"}\n    className={cn(\n      \"flex size-9 items-center justify-center rounded-xl\",\n      disabled\n        ? \"bg-muted text-muted-foreground\"\n        : \"bg-foreground text-background\"\n    )}\n    disabled={disabled}\n    onClick={onClick}\n    transition={shouldReduceMotion ? { duration: 0 } : SPRING_DEFAULT}\n    type=\"button\"\n    whileHover={disabled || shouldReduceMotion ? undefined : { scale: 1.05 }}\n    whileTap={disabled || shouldReduceMotion ? undefined : { scale: 0.95 }}\n  >\n    {/* Real lucide glyphs, swapped rather than morphed.\n        An earlier version interpolated one hand-authored `d` into another, which\n        bought a nice fold at the cost of not being a lucide icon at all — and a\n        filled wedge reads as \"play\", not \"send\". A thin stroked arrow is the\n        convention, and lucide's own geometry is what the rest of the library\n        uses. The swap is scale-and-fade so it still reads as one control. */}\n    <AnimatePresence initial={false} mode=\"popLayout\">\n      <motion.span\n        animate={{ opacity: 1, scale: 1 }}\n        className=\"flex items-center justify-center\"\n        exit={\n          shouldReduceMotion\n            ? { opacity: 0, transition: { duration: 0 } }\n            : { opacity: 0, scale: 0.6 }\n        }\n        initial={\n          shouldReduceMotion ? { opacity: 1 } : { opacity: 0, scale: 0.6 }\n        }\n        key={isStreaming ? \"stop\" : \"send\"}\n        transition={\n          shouldReduceMotion\n            ? { duration: 0 }\n            : { duration: 0.18 / speed, ease: EASE_OUT }\n        }\n      >\n        {isStreaming ? (\n          <Square\n            aria-hidden=\"true\"\n            className=\"fill-current\"\n            size={STOP_ICON_SIZE}\n            strokeWidth={0}\n          />\n        ) : (\n          <ArrowUp aria-hidden=\"true\" size={SUBMIT_ICON_SIZE} />\n        )}\n      </motion.span>\n    </AnimatePresence>\n  </motion.button>\n);\n\nexport default AIPromptInput;\n","path":"index.tsx","target":"components/smoothui/ai-prompt-input/index.tsx","type":"registry:ui"}],"name":"ai-prompt-input","registryDependencies":["https://smoothui.dev/r/ai-core.json"],"title":"Ai Prompt Input","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["lucide-react","motion"],"description":"Collapsible AI reasoning trace that shimmers only while the model works, reports how long it took and gets out of the way.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { ChevronRight } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { type ReactNode, useEffect, useRef, useState } from \"react\";\n\nconst SPRING_DEFAULT = {\n  bounce: 0.1,\n  duration: 0.25,\n  type: \"spring\" as const,\n};\nconst EASE_OUT = [0.23, 1, 0.32, 1] as const;\n/**\n * A beat before auto-collapsing. Snapping shut the instant the trace finishes\n * makes it feel like the content was yanked away mid-read.\n */\nconst AUTO_COLLAPSE_DELAY_MS = 600;\nconst MS_PER_SECOND = 1000;\nconst SHIMMER_SECONDS = 1.8;\n\nexport type AIReasoningProps = {\n  children: ReactNode;\n  className?: string;\n  /**\n   * Collapse itself once `isStreaming` goes false. The trace is scaffolding —\n   * interesting while it happens, clutter once the answer has landed.\n   */\n  collapseWhenDone?: boolean;\n  /** Start expanded. */\n  defaultOpen?: boolean;\n  /** Seconds the model spent. Omit to have the component time it itself. */\n  duration?: number;\n  isStreaming?: boolean;\n};\n\n/**\n * Collapsible reasoning trace.\n *\n * The summary line shimmers only while the model is actually working, so the\n * shimmer means something rather than being decoration. When the trace ends it\n * settles, reports how long it took, and gets out of the way.\n */\nconst AIReasoning = ({\n  children,\n  className,\n  collapseWhenDone = true,\n  defaultOpen,\n  duration,\n  isStreaming = false,\n}: AIReasoningProps) => {\n  const shouldReduceMotion = useReducedMotion();\n  const [isOpen, setIsOpen] = useState(defaultOpen ?? isStreaming);\n  // Once someone opens or closes it by hand, stop deciding for them.\n  const [isUserControlled, setIsUserControlled] = useState(false);\n\n  const startedAtRef = useRef<number | null>(null);\n  const [measuredSeconds, setMeasuredSeconds] = useState<number | null>(null);\n\n  useEffect(() => {\n    if (isStreaming) {\n      startedAtRef.current = Date.now();\n      setMeasuredSeconds(null);\n      if (!isUserControlled) {\n        setIsOpen(true);\n      }\n      return;\n    }\n    if (startedAtRef.current !== null) {\n      setMeasuredSeconds((Date.now() - startedAtRef.current) / MS_PER_SECOND);\n      startedAtRef.current = null;\n    }\n  }, [isStreaming, isUserControlled]);\n\n  useEffect(() => {\n    if (isStreaming || !collapseWhenDone || isUserControlled) {\n      return;\n    }\n    const timeout = setTimeout(() => setIsOpen(false), AUTO_COLLAPSE_DELAY_MS);\n    return () => clearTimeout(timeout);\n  }, [collapseWhenDone, isStreaming, isUserControlled]);\n\n  const seconds = duration ?? measuredSeconds;\n  const summary = (() => {\n    if (isStreaming) {\n      return \"Thinking\";\n    }\n    if (seconds !== null) {\n      return `Thought for ${seconds.toFixed(1)}s`;\n    }\n    return \"Reasoning\";\n  })();\n\n  return (\n    <div className={cn(\"w-full\", className)}>\n      <button\n        aria-expanded={isOpen}\n        className=\"group flex w-full cursor-pointer items-center gap-1.5 rounded-lg py-1 text-left text-muted-foreground text-sm transition-colors hover:text-foreground\"\n        onClick={() => {\n          setIsUserControlled(true);\n          setIsOpen((current) => !current);\n        }}\n        type=\"button\"\n      >\n        <motion.span\n          animate={{ rotate: isOpen ? 90 : 0 }}\n          className=\"flex size-4 items-center justify-center\"\n          transition={shouldReduceMotion ? { duration: 0 } : SPRING_DEFAULT}\n        >\n          <ChevronRight aria-hidden=\"true\" size={14} />\n        </motion.span>\n\n        {/* The shimmer is the \"still working\" signal, so it exists only while\n            streaming. A permanently shimmering label is just decoration. */}\n        {isStreaming && !shouldReduceMotion ? (\n          <span\n            className=\"bg-[length:200%_100%] bg-clip-text text-transparent\"\n            style={{\n              animation: `ai-reasoning-shimmer ${SHIMMER_SECONDS}s linear infinite`,\n              backgroundImage:\n                \"linear-gradient(90deg, currentColor 0%, currentColor 35%, color-mix(in oklab, currentColor 25%, transparent) 50%, currentColor 65%, currentColor 100%)\",\n            }}\n          >\n            {summary}\n          </span>\n        ) : (\n          <span>{summary}</span>\n        )}\n      </button>\n\n      <style>{`\n        @keyframes ai-reasoning-shimmer {\n          from { background-position: 200% 0; }\n          to { background-position: -200% 0; }\n        }\n      `}</style>\n\n      <AnimatePresence initial={false}>\n        {isOpen ? (\n          <motion.div\n            animate={{ height: \"auto\", opacity: 1 }}\n            className=\"overflow-hidden\"\n            exit={\n              shouldReduceMotion\n                ? { opacity: 0, transition: { duration: 0 } }\n                : { height: 0, opacity: 0 }\n            }\n            initial={\n              shouldReduceMotion\n                ? { height: \"auto\", opacity: 1 }\n                : { height: 0, opacity: 0 }\n            }\n            transition={\n              shouldReduceMotion\n                ? { duration: 0 }\n                : {\n                    height: SPRING_DEFAULT,\n                    opacity: { duration: 0.18, ease: EASE_OUT },\n                  }\n            }\n          >\n            {/* The rule doubles as a margin: the trace reads as an aside, not as\n                part of the answer. */}\n            <div className=\"mt-1 ml-[7px] border-border border-l pl-4 text-muted-foreground text-sm leading-relaxed\">\n              {children}\n            </div>\n          </motion.div>\n        ) : null}\n      </AnimatePresence>\n    </div>\n  );\n};\n\nexport default AIReasoning;\n","path":"index.tsx","target":"components/smoothui/ai-reasoning/index.tsx","type":"registry:ui"}],"name":"ai-reasoning","registryDependencies":[],"title":"Ai Reasoning","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"Streaming assistant text where words animate in as they arrive, with a caret that rides the last glyph and inline citation pills.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { Fragment, useEffect, useRef } from \"react\";\n\nconst EASE_OUT = [0.23, 1, 0.32, 1] as const;\n/**\n * Hoisted and frozen. Motion restarts an animation whenever the transition it is\n * given changes, and this component re-renders on every token — so the object\n * has to be the same reference every time.\n */\nconst WORD_TRANSITION = { duration: 0.22, ease: EASE_OUT } as const;\nconst WORD_BLUR_PX = 4;\n/** `[1]` style markers become citation pills. */\nconst CITATION_MARKER = /^\\[(\\d+)\\]$/;\n/**\n * Split on whitespace *and* on markers, so a marker is its own token even when\n * punctuation is glued to it — `compute [1],` has to yield `[1]` and `,`\n * separately or the pill never matches.\n */\nconst TOKEN_SPLIT = /(\\s+|\\[\\d+\\])/;\n/** Anything without a letter or digit is punctuation and is not animated. */\nconst HAS_WORD_CHARACTER = /[\\p{L}\\p{N}]/u;\nconst WHITESPACE_ONLY = /^\\s+$/;\n\nexport type AIResponseCitation = {\n  id: string;\n  /** The number shown in the pill, matching the `[n]` marker in the text. */\n  index: number;\n  title: string;\n  /**\n   * Where the source lives, when it lives anywhere.\n   *\n   * Optional on purpose: most retrieval is over internal documents that have no\n   * public URL, and a required field there just pushes people into inventing\n   * `example.com` links that go nowhere. Without a url the pill renders as plain\n   * text instead of a dead link.\n   */\n  url?: string;\n};\n\nexport type AIResponseProps = {\n  /** Sources referenced by `[n]` markers in the text. */\n  citations?: AIResponseCitation[];\n  className?: string;\n  /** Shows a caret after the last word. */\n  isStreaming?: boolean;\n  /** The response so far. Re-render it as it grows. */\n  text: string;\n};\n\ntype Token = {\n  citation?: AIResponseCitation;\n  value: string;\n};\n\nconst tokenize = (text: string, citations: AIResponseCitation[]): Token[] =>\n  text\n    .split(TOKEN_SPLIT)\n    .filter((value) => value !== \"\")\n    .map((value) => {\n      const match = value.match(CITATION_MARKER);\n      if (!match) {\n        return { value };\n      }\n      const index = Number(match[1]);\n      const citation = citations.find((entry) => entry.index === index);\n      return citation ? { citation, value } : { value };\n    });\n\n/**\n * Streaming assistant text.\n *\n * Words animate in as they *arrive*, not on a fixed timer — the component\n * remembers how many tokens it had last render and only animates the new ones.\n * A timer-driven typewriter drifts out of step with the real stream and starts\n * lying about how fast the model is answering.\n */\nconst AIResponse = ({\n  citations = [],\n  className,\n  isStreaming = false,\n  text,\n}: AIResponseProps) => {\n  const shouldReduceMotion = useReducedMotion();\n  const tokens = tokenize(text, citations);\n\n  // How many tokens had already been painted before this render. Everything at\n  // or after this index is new and gets the entrance.\n  const paintedRef = useRef(0);\n  const firstNewIndex = paintedRef.current;\n\n  useEffect(() => {\n    paintedRef.current = tokens.length;\n  }, [tokens.length]);\n\n  return (\n    <p\n      className={cn(\n        \"text-pretty text-foreground text-sm leading-relaxed\",\n        className\n      )}\n    >\n      {tokens.map((token, index) => {\n        const isNew = index >= firstNewIndex;\n        // Keyed by position only. Keying by text too would remount the last word\n        // every time a token extends it, so it would re-animate on every frame of\n        // the stream.\n        const key = index;\n\n        // Whitespace and bare punctuation stay as text. Wrapping a comma in its\n        // own inline-block would let the line break between a word and its\n        // punctuation.\n        if (\n          WHITESPACE_ONLY.test(token.value) ||\n          !(token.citation || HAS_WORD_CHARACTER.test(token.value))\n        ) {\n          return <Fragment key={key}>{token.value}</Fragment>;\n        }\n\n        if (token.citation) {\n          return (\n            <AIResponseCitationPill\n              citation={token.citation}\n              isNew={isNew}\n              key={key}\n              shouldReduceMotion={Boolean(shouldReduceMotion)}\n            />\n          );\n        }\n\n        return (\n          <motion.span\n            animate={{ filter: \"blur(0px)\", opacity: 1, y: 0 }}\n            className=\"inline-block\"\n            initial={\n              isNew && !shouldReduceMotion\n                ? { filter: `blur(${WORD_BLUR_PX}px)`, opacity: 0, y: 2 }\n                : false\n            }\n            key={key}\n            // No stagger delay, deliberately. The transition object has to stay\n            // identical across renders: a delay derived from the render-time\n            // index goes negative as the text grows, and the entrance then never\n            // resolves — words stay blurred forever. Token arrival is the stagger.\n            transition={shouldReduceMotion ? { duration: 0 } : WORD_TRANSITION}\n          >\n            {token.value}\n          </motion.span>\n        );\n      })}\n      {isStreaming ? (\n        <AIResponseCaret shouldReduceMotion={shouldReduceMotion} />\n      ) : null}\n    </p>\n  );\n};\n\nconst AIResponseCaret = ({\n  shouldReduceMotion,\n}: {\n  shouldReduceMotion: boolean | null;\n}) => (\n  // Inline rather than absolutely positioned, so it rides the last glyph for\n  // free and never has to be told where the text ended.\n  <motion.span\n    animate={shouldReduceMotion ? { opacity: 1 } : { opacity: [1, 0.15, 1] }}\n    aria-hidden=\"true\"\n    className=\"ml-0.5 inline-block h-[1em] w-[2px] translate-y-[0.15em] rounded-full bg-current align-baseline\"\n    transition={\n      shouldReduceMotion\n        ? { duration: 0 }\n        : { duration: 1, ease: \"linear\", repeat: Number.POSITIVE_INFINITY }\n    }\n  />\n);\n\nconst AIResponseCitationPill = ({\n  citation,\n  isNew,\n  shouldReduceMotion,\n}: {\n  citation: AIResponseCitation;\n  isNew: boolean;\n  shouldReduceMotion: boolean;\n}) => {\n  const shared = {\n    animate: { opacity: 1, scale: 1 },\n    // No left margin: the marker is already preceded by a space in the text, so\n    // adding one here doubles the gap.\n    className:\n      \"mr-0.5 inline-flex h-4 min-w-4 items-center justify-center rounded-full border border-border bg-muted px-1 align-super font-medium text-[10px] text-muted-foreground no-underline\",\n    initial:\n      isNew && !shouldReduceMotion\n        ? { opacity: 0, scale: 0.6 }\n        : (false as const),\n    title: citation.title,\n    transition: shouldReduceMotion\n      ? { duration: 0 }\n      : { bounce: 0.1, duration: 0.25, type: \"spring\" as const },\n  };\n\n  // An internal document has nowhere to go, so it is not dressed up as a link —\n  // no hover affordance, no pointer, nothing to click and be disappointed by.\n  if (!citation.url) {\n    return <motion.span {...shared}>{citation.index}</motion.span>;\n  }\n\n  return (\n    <motion.a\n      {...shared}\n      className={`${shared.className} transition-colors hover:border-foreground/30 hover:text-foreground`}\n      href={citation.url}\n      rel=\"noopener noreferrer\"\n      target=\"_blank\"\n    >\n      {citation.index}\n    </motion.a>\n  );\n};\n\nexport default AIResponse;\n","path":"index.tsx","target":"components/smoothui/ai-response/index.tsx","type":"registry:ui"}],"name":"ai-response","registryDependencies":[],"title":"Ai Response","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["lucide-react","motion"],"description":"Favicon stack that fans out on hover and expands into a source list, with each favicon morphing into its row.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { ChevronRight, Globe } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { type ReactNode, useId, useState } from \"react\";\n\nconst SPRING_DEFAULT = {\n  bounce: 0.1,\n  duration: 0.25,\n  type: \"spring\" as const,\n};\nconst EASE_OUT = [0.23, 1, 0.32, 1] as const;\n/** Favicons shown in the collapsed stack before the \"+n\" chip. */\nconst STACK_LIMIT = 3;\n/** Overlap of the collapsed stack, and how far it fans on hover. */\nconst STACK_OVERLAP_PX = 8;\nconst FAN_GAP_PX = 3;\nconst ROW_STAGGER = 0.035;\n\nexport type AISource = {\n  /**\n   * The source's mark — a real isotype or an `<img>`, never a letter.\n   *\n   * A node rather than a URL so a brand's own SVG can be passed straight in.\n   * When omitted the component draws a neutral globe: an honest \"unknown host\"\n   * icon beats a fabricated one-letter badge pretending to be a logo.\n   */\n  favicon?: ReactNode;\n  id: string;\n  /** Short excerpt or description. */\n  snippet?: string;\n  title: string;\n  url: string;\n};\n\nexport type AISourcesProps = {\n  className?: string;\n  /** Start expanded. */\n  defaultOpen?: boolean;\n  /** Label before the stack, e.g. \"Sources\". */\n  label?: string;\n  sources: AISource[];\n};\n\nconst hostOf = (url: string): string => {\n  try {\n    return new URL(url).hostname.replace(/^www\\./, \"\");\n  } catch {\n    return url;\n  }\n};\n\nconst Favicon = ({ size, source }: { size: number; source: AISource }) => (\n  <span\n    className=\"flex items-center justify-center overflow-hidden rounded-full border border-border bg-background text-muted-foreground\"\n    style={{ height: size, width: size }}\n  >\n    {source.favicon ? (\n      // Sized here so any mark fits: an `<img>` from a logo service, or a repo\n      // isotype SVG, which ships as `fill=\"none\"` and expects the host to pick\n      // the colour — the same contract the logo-cloud blocks rely on.\n      <span className=\"flex size-full items-center justify-center *:size-full *:fill-foreground *:object-cover\">\n        {source.favicon}\n      </span>\n    ) : (\n      <Globe aria-hidden=\"true\" size={size * 0.6} />\n    )}\n  </span>\n);\n\n/**\n * The sources behind an answer.\n *\n * Collapsed to a stack of overlapping favicons, because provenance should be\n * available rather than loud. Hovering fans the stack apart as a hint that it is\n * a set and not a single badge; opening it moves each favicon into its own row —\n * a shared layout id, so the icon the eye was tracking is the icon that lands.\n */\nconst AISources = ({\n  className,\n  defaultOpen = false,\n  label = \"Sources\",\n  sources,\n}: AISourcesProps) => {\n  const shouldReduceMotion = useReducedMotion();\n  const [isOpen, setIsOpen] = useState(defaultOpen);\n  const [isHovered, setIsHovered] = useState(false);\n  const layoutPrefix = useId();\n\n  const stacked = sources.slice(0, STACK_LIMIT);\n  const overflow = sources.length - stacked.length;\n  const fanned = isHovered && !isOpen && !shouldReduceMotion;\n\n  return (\n    <div className={cn(\"w-full\", className)}>\n      <button\n        aria-expanded={isOpen}\n        className=\"flex cursor-pointer items-center gap-2 rounded-lg py-1 text-muted-foreground text-sm transition-colors hover:text-foreground\"\n        onBlur={() => setIsHovered(false)}\n        onClick={() => setIsOpen((current) => !current)}\n        onFocus={() => setIsHovered(true)}\n        onMouseEnter={() => setIsHovered(true)}\n        onMouseLeave={() => setIsHovered(false)}\n        type=\"button\"\n      >\n        <span className=\"flex items-center\">\n          {stacked.map((source, index) => (\n            <motion.span\n              animate={{\n                marginLeft:\n                  index === 0 ? 0 : fanned ? FAN_GAP_PX : -STACK_OVERLAP_PX,\n              }}\n              className=\"relative block\"\n              key={source.id}\n              // The same layoutId as the row's favicon, so the icon travels\n              // instead of one disappearing while another appears.\n              layoutId={isOpen ? undefined : `${layoutPrefix}-${source.id}`}\n              style={{ zIndex: stacked.length - index }}\n              transition={shouldReduceMotion ? { duration: 0 } : SPRING_DEFAULT}\n            >\n              <Favicon size={18} source={source} />\n            </motion.span>\n          ))}\n          {overflow > 0 && (\n            <span className=\"ml-1 tabular-nums\">+{overflow}</span>\n          )}\n        </span>\n\n        <span>{label}</span>\n\n        <motion.span\n          animate={{ rotate: isOpen ? 90 : 0 }}\n          className=\"flex size-4 items-center justify-center\"\n          transition={shouldReduceMotion ? { duration: 0 } : SPRING_DEFAULT}\n        >\n          <ChevronRight aria-hidden=\"true\" size={14} />\n        </motion.span>\n      </button>\n\n      <AnimatePresence initial={false}>\n        {isOpen ? (\n          <motion.ul\n            animate={{ height: \"auto\", opacity: 1 }}\n            className=\"mt-1 list-none overflow-hidden\"\n            exit={\n              shouldReduceMotion\n                ? { opacity: 0, transition: { duration: 0 } }\n                : { height: 0, opacity: 0 }\n            }\n            initial={\n              shouldReduceMotion\n                ? { height: \"auto\", opacity: 1 }\n                : { height: 0, opacity: 0 }\n            }\n            transition={\n              shouldReduceMotion\n                ? { duration: 0 }\n                : {\n                    height: SPRING_DEFAULT,\n                    opacity: { duration: 0.18, ease: EASE_OUT },\n                  }\n            }\n          >\n            {sources.map((source, index) => (\n              <motion.li\n                animate={{ opacity: 1, y: 0 }}\n                initial={\n                  shouldReduceMotion\n                    ? { opacity: 1, y: 0 }\n                    : { opacity: 0, y: 4 }\n                }\n                key={source.id}\n                transition={\n                  shouldReduceMotion\n                    ? { duration: 0 }\n                    : { ...SPRING_DEFAULT, delay: index * ROW_STAGGER }\n                }\n              >\n                <a\n                  className=\"flex items-start gap-2.5 rounded-lg px-1 py-1.5 no-underline transition-colors hover:bg-muted\"\n                  href={source.url}\n                  rel=\"noopener noreferrer\"\n                  target=\"_blank\"\n                >\n                  <motion.span\n                    className=\"mt-0.5 block shrink-0\"\n                    layoutId={`${layoutPrefix}-${source.id}`}\n                    transition={\n                      shouldReduceMotion ? { duration: 0 } : SPRING_DEFAULT\n                    }\n                  >\n                    <Favicon size={18} source={source} />\n                  </motion.span>\n\n                  <span className=\"min-w-0\">\n                    <span className=\"block truncate font-medium text-foreground text-sm\">\n                      {source.title}\n                    </span>\n                    <span className=\"block truncate text-muted-foreground text-xs\">\n                      {source.snippet ?? hostOf(source.url)}\n                    </span>\n                  </span>\n                </a>\n              </motion.li>\n            ))}\n          </motion.ul>\n        ) : null}\n      </AnimatePresence>\n    </div>\n  );\n};\n\nexport default AISources;\n","path":"index.tsx","target":"components/smoothui/ai-sources/index.tsx","type":"registry:ui"}],"name":"ai-sources","registryDependencies":[],"title":"Ai Sources","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["lucide-react","motion"],"description":"Prompt suggestion chips that radiate in from the centre of the row rather than sweeping left to right.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useMemo } from \"react\";\n\nconst SPRING_DEFAULT = {\n  bounce: 0.1,\n  duration: 0.25,\n  type: \"spring\" as const,\n};\nconst STAGGER_SECONDS = 0.045;\n\nexport type AISuggestion = {\n  id: string;\n  label: string;\n};\n\nexport type AISuggestionsProps = {\n  className?: string;\n  /** Optional heading, e.g. \"Follow-ups\". */\n  label?: string;\n  onSelect?: (suggestion: AISuggestion) => void;\n  suggestions: AISuggestion[];\n};\n\n/**\n * Distance from the middle of the row, so the stagger radiates outwards from the\n * centre instead of sweeping left to right.\n *\n * A left-to-right sweep implies reading order and priority — that the first chip\n * matters most. These are alternatives of equal weight, and radiating from the\n * centre says so.\n */\nconst centreOutOrder = (count: number): number[] => {\n  const middle = (count - 1) / 2;\n  return Array.from({ length: count }, (_, index) => Math.abs(index - middle));\n};\n\n/**\n * Prompt suggestion chips.\n *\n * Chips are the empty state of a chat and the follow-up row after an answer, so\n * they need to arrive without feeling like a notification.\n */\nconst AISuggestions = ({\n  className,\n  label,\n  onSelect,\n  suggestions,\n}: AISuggestionsProps) => {\n  const shouldReduceMotion = useReducedMotion();\n  const delays = useMemo(\n    () => centreOutOrder(suggestions.length),\n    [suggestions.length]\n  );\n\n  return (\n    <div className={cn(\"flex w-full flex-col gap-2\", className)}>\n      {label ? (\n        <p className=\"text-muted-foreground text-xs uppercase tracking-wide\">\n          {label}\n        </p>\n      ) : null}\n\n      <ul className=\"flex list-none flex-wrap gap-2\">\n        <AnimatePresence initial>\n          {suggestions.map((suggestion, index) => (\n            <motion.li\n              animate={{ opacity: 1, scale: 1, y: 0 }}\n              exit={\n                shouldReduceMotion\n                  ? { opacity: 0, transition: { duration: 0 } }\n                  : { opacity: 0, scale: 0.94 }\n              }\n              initial={\n                shouldReduceMotion\n                  ? { opacity: 1, scale: 1, y: 0 }\n                  : { opacity: 0, scale: 0.94, y: 6 }\n              }\n              key={suggestion.id}\n              layout={!shouldReduceMotion}\n              transition={\n                shouldReduceMotion\n                  ? { duration: 0 }\n                  : {\n                      ...SPRING_DEFAULT,\n                      delay: (delays[index] ?? 0) * STAGGER_SECONDS,\n                    }\n              }\n            >\n              <motion.button\n                className=\"cursor-pointer rounded-full border border-border bg-background px-3 py-1.5 text-left text-foreground text-sm transition-colors hover:border-foreground/30 hover:bg-muted\"\n                onClick={() => onSelect?.(suggestion)}\n                transition={\n                  shouldReduceMotion ? { duration: 0 } : SPRING_DEFAULT\n                }\n                type=\"button\"\n                whileTap={shouldReduceMotion ? undefined : { scale: 0.97 }}\n              >\n                {suggestion.label}\n              </motion.button>\n            </motion.li>\n          ))}\n        </AnimatePresence>\n      </ul>\n    </div>\n  );\n};\n\nexport default AISuggestions;\n","path":"index.tsx","target":"components/smoothui/ai-suggestions/index.tsx","type":"registry:ui"}],"name":"ai-suggestions","registryDependencies":[],"title":"Ai Suggestions","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["lucide-react","motion"],"description":"Live agent plan with nested steps, a drawn checkmark on completion and a travelling underline on whatever is running.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { Fragment } from \"react\";\n\nconst SPRING_DEFAULT = {\n  bounce: 0.1,\n  duration: 0.25,\n  type: \"spring\" as const,\n};\nconst EASE_IN_OUT = [0.645, 0.045, 0.355, 1] as const;\nconst CHECK_PATH = \"M 3.5 7.5 L 6 10 L 10.5 4.5\";\nconst BOX_SIZE = 14;\nconst UNDERLINE_SECONDS = 1.4;\n\nexport type AITaskStatus = \"pending\" | \"running\" | \"done\" | \"failed\";\n\nexport type AITask = {\n  /** Nested sub-steps, one level. */\n  children?: AITask[];\n  id: string;\n  label: string;\n  /** Short right-aligned note, e.g. \"12/12\" or \"3 files\". */\n  note?: string;\n  status: AITaskStatus;\n};\n\nexport type AITaskListProps = {\n  className?: string;\n  /** Heading text. The counts are derived, never passed in. */\n  label?: string;\n  tasks: AITask[];\n};\n\nconst flatten = (tasks: AITask[]): AITask[] =>\n  tasks.flatMap((task) => [task, ...flatten(task.children ?? [])]);\n\nconst SUCCESS_COLOR = \"oklch(72% 0.17 150)\";\nconst DANGER_COLOR = \"oklch(63% 0.21 25)\";\n\nconst boxStroke = (status: AITaskStatus): string => {\n  if (status === \"failed\") {\n    return DANGER_COLOR;\n  }\n  if (status === \"done\") {\n    return SUCCESS_COLOR;\n  }\n  return \"currentColor\";\n};\n\nconst TaskBox = ({\n  status,\n  shouldReduceMotion,\n}: {\n  shouldReduceMotion: boolean;\n  status: AITaskStatus;\n}) => {\n  const isDone = status === \"done\";\n  const isFailed = status === \"failed\";\n\n  return (\n    <span className=\"mt-0.5 flex size-3.5 shrink-0 items-center justify-center\">\n      <svg\n        aria-hidden=\"true\"\n        className=\"size-3.5\"\n        viewBox={`0 0 ${BOX_SIZE} ${BOX_SIZE}`}\n      >\n        <motion.rect\n          animate={{\n            fillOpacity: isDone ? 0.12 : 0,\n            stroke: boxStroke(status),\n          }}\n          fill={isFailed ? DANGER_COLOR : SUCCESS_COLOR}\n          height={12}\n          rx={3.5}\n          strokeWidth={1.4}\n          transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.2 }}\n          width={12}\n          x={1}\n          y={1}\n        />\n        {isDone && (\n          // Drawn, not faded: a check that draws itself reads as the act of\n          // ticking the box rather than a state that was always there.\n          // Drawn with a CSS keyframe rather than a motion value.\n          //\n          // Neither motion's `pathLength` shorthand nor an explicit\n          // `strokeDashoffset` animation resolved on these paths — the dash\n          // stayed pinned at its initial value and the check rendered as a stub.\n          // A keyframe on mount is deterministic, and `prefers-reduced-motion`\n          // handles the accessible case in CSS with no JS branch at all.\n          <path\n            className=\"ai-task-draw\"\n            d={CHECK_PATH}\n            fill=\"none\"\n            pathLength={1}\n            stroke={SUCCESS_COLOR}\n            strokeDasharray=\"1 1\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n            strokeWidth={1.8}\n          />\n        )}\n        {isFailed && (\n          <path\n            className=\"ai-task-draw\"\n            d=\"M 5 5 L 9 9 M 9 5 L 5 9\"\n            fill=\"none\"\n            pathLength={1}\n            stroke={DANGER_COLOR}\n            strokeDasharray=\"1 1\"\n            strokeLinecap=\"round\"\n            strokeWidth={1.8}\n          />\n        )}\n      </svg>\n    </span>\n  );\n};\n\nconst TaskRow = ({\n  depth,\n  shouldReduceMotion,\n  task,\n}: {\n  depth: number;\n  shouldReduceMotion: boolean;\n  task: AITask;\n}) => {\n  const isRunning = task.status === \"running\";\n  const isDone = task.status === \"done\";\n\n  return (\n    <motion.li\n      // Completed rows settle down a pixel and lose a little contrast: they go\n      // quiet so whatever is running is the only thing asking for attention.\n      animate={{\n        opacity: isDone ? 0.65 : 1,\n        y: isDone && !shouldReduceMotion ? 1 : 0,\n      }}\n      className=\"list-none\"\n      style={{ paddingLeft: depth * 20 }}\n      transition={shouldReduceMotion ? { duration: 0 } : SPRING_DEFAULT}\n    >\n      <span className=\"relative flex items-start gap-2 py-1\">\n        <TaskBox shouldReduceMotion={shouldReduceMotion} status={task.status} />\n\n        <span className=\"min-w-0 flex-1 text-foreground text-sm leading-snug\">\n          {task.label}\n        </span>\n\n        {task.note ? (\n          <span className=\"shrink-0 text-muted-foreground text-xs tabular-nums\">\n            {task.note}\n          </span>\n        ) : null}\n\n        {isRunning && !shouldReduceMotion && (\n          // A travelling underline under the active row only. One moving thing\n          // at a time is what makes \"which step is live\" readable at a glance.\n          <motion.span\n            animate={{ backgroundPositionX: [\"0%\", \"200%\"] }}\n            className=\"pointer-events-none absolute inset-x-0 bottom-0 h-px\"\n            style={{\n              backgroundImage:\n                \"linear-gradient(90deg, transparent 0%, currentColor 50%, transparent 100%)\",\n              backgroundSize: \"50% 100%\",\n              opacity: 0.5,\n            }}\n            transition={{\n              duration: UNDERLINE_SECONDS,\n              ease: EASE_IN_OUT,\n              repeat: Number.POSITIVE_INFINITY,\n            }}\n          />\n        )}\n      </span>\n    </motion.li>\n  );\n};\n\n/**\n * A plan an agent works through.\n *\n * Header counts are derived from the tasks, so the summary can never disagree\n * with the rows — a \"3/7\" that has drifted from what is on screen destroys trust\n * in the whole panel.\n */\nconst AITaskList = ({ className, label = \"Plan\", tasks }: AITaskListProps) => {\n  const shouldReduceMotion = Boolean(useReducedMotion());\n  const all = flatten(tasks);\n  const done = all.filter((task) => task.status === \"done\").length;\n\n  return (\n    <div\n      className={cn(\n        \"w-full rounded-xl border border-border bg-background p-3\",\n        className\n      )}\n    >\n      <div className=\"mb-1.5 flex items-baseline justify-between\">\n        <p className=\"font-medium text-foreground text-sm\">{label}</p>\n        <p className=\"text-muted-foreground text-xs tabular-nums\">\n          {done}/{all.length}\n        </p>\n      </div>\n\n      <style>{`\n        .ai-task-draw { stroke-dashoffset: 0; }\n        @media (prefers-reduced-motion: no-preference) {\n          .ai-task-draw {\n            animation: ai-task-draw 200ms cubic-bezier(0.23, 1, 0.32, 1) both;\n          }\n        }\n        @keyframes ai-task-draw {\n          from { stroke-dashoffset: 1; }\n          to { stroke-dashoffset: 0; }\n        }\n      `}</style>\n\n      <ul className=\"list-none\">\n        {tasks.map((task) => (\n          <Fragment key={task.id}>\n            <TaskRow\n              depth={0}\n              shouldReduceMotion={shouldReduceMotion}\n              task={task}\n            />\n            {task.children?.map((child) => (\n              <TaskRow\n                depth={1}\n                key={child.id}\n                shouldReduceMotion={shouldReduceMotion}\n                task={child}\n              />\n            ))}\n          </Fragment>\n        ))}\n      </ul>\n    </div>\n  );\n};\n\nexport default AITaskList;\n","path":"index.tsx","target":"components/smoothui/ai-task-list/index.tsx","type":"registry:ui"}],"name":"ai-task-list","registryDependencies":[],"title":"Ai Task List","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["lucide-react","motion"],"description":"Collapsible tool invocation whose status badge is one ring that evolves — breathing, spinning, then drawing a check or a cross.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { ChevronRight } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { type ReactNode, useState } from \"react\";\n\nconst SPRING_DEFAULT = {\n  bounce: 0.1,\n  duration: 0.25,\n  type: \"spring\" as const,\n};\nconst EASE_OUT = [0.23, 1, 0.32, 1] as const;\nconst BADGE_VIEWBOX = 24;\nconst BADGE_CENTER = BADGE_VIEWBOX / 2;\nconst RING_RADIUS = 8;\nconst CHECK_PATH = \"M 8.5 12.2 L 11 14.8 L 15.8 9.6\";\nconst CROSS_PATHS = [\"M 9 9 L 15 15\", \"M 15 9 L 9 15\"] as const;\nconst SPIN_SECONDS = 0.9;\nconst PENDING_PULSE_SECONDS = 1.6;\n\nexport type AIToolCallStatus = \"pending\" | \"running\" | \"success\" | \"error\";\n\nexport type AIToolCallProps = {\n  /** Arguments the tool was called with. Rendered as-is. */\n  args?: ReactNode;\n  className?: string;\n  defaultOpen?: boolean;\n  /** Tool name, e.g. `search_web`. */\n  name: string;\n  /** What the tool returned. */\n  result?: ReactNode;\n  status?: AIToolCallStatus;\n  /** Short right-aligned note, e.g. \"3 files\" or \"1.2s\". */\n  summary?: string;\n};\n\nconst STATUS_LABEL: Record<AIToolCallStatus, string> = {\n  error: \"Failed\",\n  pending: \"Queued\",\n  running: \"Running\",\n  success: \"Done\",\n};\n\n/**\n * The status badge is **one ring that changes behaviour**, not four icons that\n * swap places.\n *\n * `pending` breathes, `running` spins as a gap in the same ring, `success` keeps\n * the ring and draws a check inside it, `error` keeps the ring and draws a\n * cross. Because the ring never unmounts, the eye tracks a single object through\n * the whole lifecycle instead of watching icons pop in and out.\n */\nconst AIToolCallBadge = ({\n  status,\n  shouldReduceMotion,\n}: {\n  shouldReduceMotion: boolean;\n  status: AIToolCallStatus;\n}) => {\n  const isRunning = status === \"running\";\n  const isPending = status === \"pending\";\n\n  const ringColor = (() => {\n    if (status === \"success\") {\n      return \"oklch(72% 0.17 150)\";\n    }\n    if (status === \"error\") {\n      return \"oklch(63% 0.21 25)\";\n    }\n    return \"currentColor\";\n  })();\n\n  return (\n    <span className=\"relative flex size-5 shrink-0 items-center justify-center text-muted-foreground\">\n      <svg\n        aria-hidden=\"true\"\n        className=\"size-5 overflow-visible\"\n        viewBox={`0 0 ${BADGE_VIEWBOX} ${BADGE_VIEWBOX}`}\n      >\n        <motion.g\n          animate={\n            shouldReduceMotion || !isRunning ? undefined : { rotate: 360 }\n          }\n          style={{ transformBox: \"view-box\", transformOrigin: \"center\" }}\n          transition={{\n            duration: SPIN_SECONDS,\n            ease: \"linear\",\n            repeat: Number.POSITIVE_INFINITY,\n          }}\n        >\n          <motion.circle\n            animate={{\n              stroke: ringColor,\n              // Running opens a gap in the ring; everything else closes it.\n              strokeDasharray: isRunning ? \"0.68 0.32\" : \"1 0\",\n              strokeOpacity:\n                isPending && !shouldReduceMotion ? [0.35, 1, 0.35] : 1,\n            }}\n            cx={BADGE_CENTER}\n            cy={BADGE_CENTER}\n            fill=\"none\"\n            pathLength={1}\n            r={RING_RADIUS}\n            strokeLinecap=\"round\"\n            strokeWidth={2}\n            transition={{\n              stroke: shouldReduceMotion\n                ? { duration: 0 }\n                : { duration: 0.25, ease: EASE_OUT },\n              strokeDasharray: shouldReduceMotion\n                ? { duration: 0 }\n                : { duration: 0.25, ease: EASE_OUT },\n              strokeOpacity: shouldReduceMotion\n                ? { duration: 0 }\n                : {\n                    duration: PENDING_PULSE_SECONDS,\n                    repeat: Number.POSITIVE_INFINITY,\n                  },\n            }}\n          />\n        </motion.g>\n\n        <AnimatePresence initial={false}>\n          {status === \"success\" && (\n            <motion.path\n              animate={{ opacity: 1, pathLength: 1 }}\n              d={CHECK_PATH}\n              exit={{ opacity: 0, transition: { duration: 0.1 } }}\n              fill=\"none\"\n              initial={\n                shouldReduceMotion\n                  ? { opacity: 1, pathLength: 1 }\n                  : { opacity: 1, pathLength: 0 }\n              }\n              key=\"check\"\n              stroke={ringColor}\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n              strokeWidth={2.2}\n              transition={\n                shouldReduceMotion\n                  ? { duration: 0 }\n                  : { duration: 0.22, ease: EASE_OUT }\n              }\n            />\n          )}\n          {status === \"error\" &&\n            CROSS_PATHS.map((path, index) => (\n              <motion.path\n                animate={{ opacity: 1, pathLength: 1 }}\n                d={path}\n                exit={{ opacity: 0, transition: { duration: 0.1 } }}\n                fill=\"none\"\n                initial={\n                  shouldReduceMotion\n                    ? { opacity: 1, pathLength: 1 }\n                    : { opacity: 1, pathLength: 0 }\n                }\n                key={path}\n                stroke={ringColor}\n                strokeLinecap=\"round\"\n                strokeWidth={2.2}\n                transition={\n                  shouldReduceMotion\n                    ? { duration: 0 }\n                    : { delay: index * 0.06, duration: 0.16, ease: EASE_OUT }\n                }\n              />\n            ))}\n        </AnimatePresence>\n      </svg>\n    </span>\n  );\n};\n\n/**\n * A single tool invocation, collapsed by default.\n *\n * Arguments and results are the kind of thing people want available but not\n * in their face, so the row stays one line until asked.\n */\nconst AIToolCall = ({\n  args,\n  className,\n  defaultOpen = false,\n  name,\n  result,\n  status = \"pending\",\n  summary,\n}: AIToolCallProps) => {\n  const shouldReduceMotion = useReducedMotion();\n  const [isOpen, setIsOpen] = useState(defaultOpen);\n  const hasDetail = Boolean(args || result);\n\n  return (\n    <div\n      className={cn(\n        \"w-full overflow-hidden rounded-xl border border-border bg-background\",\n        className\n      )}\n    >\n      <button\n        aria-expanded={hasDetail ? isOpen : undefined}\n        className=\"flex w-full cursor-pointer items-center gap-2.5 px-3 py-2 text-left\"\n        disabled={!hasDetail}\n        onClick={() => setIsOpen((current) => !current)}\n        type=\"button\"\n      >\n        <AIToolCallBadge\n          shouldReduceMotion={Boolean(shouldReduceMotion)}\n          status={status}\n        />\n\n        <span className=\"min-w-0 flex-1\">\n          <span className=\"block truncate font-medium font-mono text-foreground text-xs\">\n            {name}\n          </span>\n        </span>\n\n        {summary ? (\n          <span className=\"shrink-0 text-muted-foreground text-xs\">\n            {summary}\n          </span>\n        ) : null}\n        <span className=\"sr-only\">{STATUS_LABEL[status]}</span>\n\n        {hasDetail && (\n          <motion.span\n            animate={{ rotate: isOpen ? 90 : 0 }}\n            className=\"flex size-4 shrink-0 items-center justify-center text-muted-foreground\"\n            transition={shouldReduceMotion ? { duration: 0 } : SPRING_DEFAULT}\n          >\n            <ChevronRight aria-hidden=\"true\" size={14} />\n          </motion.span>\n        )}\n      </button>\n\n      <AnimatePresence initial={false}>\n        {isOpen && hasDetail ? (\n          <motion.div\n            animate={{ height: \"auto\", opacity: 1 }}\n            className=\"overflow-hidden\"\n            exit={\n              shouldReduceMotion\n                ? { opacity: 0, transition: { duration: 0 } }\n                : { height: 0, opacity: 0 }\n            }\n            initial={\n              shouldReduceMotion\n                ? { height: \"auto\", opacity: 1 }\n                : { height: 0, opacity: 0 }\n            }\n            transition={\n              shouldReduceMotion\n                ? { duration: 0 }\n                : {\n                    height: SPRING_DEFAULT,\n                    opacity: { duration: 0.18, ease: EASE_OUT },\n                  }\n            }\n          >\n            <div className=\"space-y-2 border-border border-t px-3 py-2.5 text-xs\">\n              {args ? (\n                <div>\n                  <p className=\"mb-1 text-[10px] text-muted-foreground uppercase tracking-wide\">\n                    Arguments\n                  </p>\n                  <div className=\"overflow-x-auto font-mono text-foreground\">\n                    {args}\n                  </div>\n                </div>\n              ) : null}\n              {result ? (\n                <div>\n                  <p className=\"mb-1 text-[10px] text-muted-foreground uppercase tracking-wide\">\n                    Result\n                  </p>\n                  <div className=\"overflow-x-auto text-foreground\">\n                    {result}\n                  </div>\n                </div>\n              ) : null}\n            </div>\n          </motion.div>\n        ) : null}\n      </AnimatePresence>\n    </div>\n  );\n};\n\nexport default AIToolCall;\n","path":"index.tsx","target":"components/smoothui/ai-tool-call/index.tsx","type":"registry:ui"}],"name":"ai-tool-call","registryDependencies":[],"title":"Ai Tool Call","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"Stack of overlapping avatars with smooth expand/collapse animation","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { useEffect, useState } from \"react\";\n\nexport type AvatarData = {\n  src: string;\n  alt: string;\n  href?: string;\n};\n\nexport type AnimatedAvatarGroupProps = {\n  avatars: AvatarData[];\n  maxVisible?: number;\n  size?: number;\n  overlap?: number;\n  className?: string;\n  expandOnHover?: boolean;\n};\n\nconst AnimatedAvatarGroup = ({\n  avatars,\n  maxVisible = 4,\n  size = 40,\n  overlap = 0.3,\n  className,\n  expandOnHover = true,\n}: AnimatedAvatarGroupProps) => {\n  const shouldReduceMotion = useReducedMotion();\n  const [isHovered, setIsHovered] = useState(false);\n  const [isHoverDevice, setIsHoverDevice] = useState(false);\n\n  useEffect(() => {\n    const mediaQuery = window.matchMedia(\"(hover: hover) and (pointer: fine)\");\n    setIsHoverDevice(mediaQuery.matches);\n\n    const handleChange = (event: MediaQueryListEvent) => {\n      setIsHoverDevice(event.matches);\n    };\n\n    mediaQuery.addEventListener(\"change\", handleChange);\n    return () => {\n      mediaQuery.removeEventListener(\"change\", handleChange);\n    };\n  }, []);\n\n  const visibleAvatars = avatars.slice(0, maxVisible);\n  const hiddenCount = avatars.length - maxVisible;\n  const hasHiddenAvatars = hiddenCount > 0;\n\n  const overlapPx = size * overlap;\n  const expanded = expandOnHover && isHoverDevice && isHovered;\n\n  const springTransition = shouldReduceMotion\n    ? { duration: 0 }\n    : { bounce: 0.1, duration: 0.25, type: \"spring\" as const };\n\n  return (\n    <motion.div\n      aria-label=\"Avatar group\"\n      className={cn(\"flex items-center\", className)}\n      onMouseEnter={() => setIsHovered(true)}\n      onMouseLeave={() => setIsHovered(false)}\n      role=\"group\"\n    >\n      {visibleAvatars.map((avatar, index) => {\n        const marginLeft = index === 0 ? 0 : expanded ? 4 : -overlapPx;\n\n        const content = (\n          <motion.img\n            alt={avatar.alt}\n            animate={\n              shouldReduceMotion\n                ? { opacity: 1 }\n                : {\n                    opacity: 1,\n                    scale: expanded ? 1.05 : 1,\n                  }\n            }\n            className=\"rounded-full object-cover\"\n            draggable={false}\n            height={size}\n            initial={\n              shouldReduceMotion ? { opacity: 1 } : { opacity: 1, scale: 1 }\n            }\n            src={avatar.src}\n            style={{\n              height: size,\n              width: size,\n            }}\n            transition={{\n              ...springTransition,\n              delay: shouldReduceMotion ? 0 : index * 0.03,\n            }}\n            width={size}\n          />\n        );\n\n        return (\n          <motion.div\n            animate={\n              shouldReduceMotion\n                ? { marginLeft, opacity: 1 }\n                : { marginLeft, opacity: 1 }\n            }\n            className=\"relative\"\n            key={avatar.src}\n            style={{\n              height: size,\n              width: size,\n              zIndex: visibleAvatars.length - index,\n            }}\n            transition={{\n              ...springTransition,\n              delay: shouldReduceMotion ? 0 : index * 0.03,\n            }}\n          >\n            <div\n              className=\"rounded-full border-2 border-background\"\n              style={{\n                height: size,\n                width: size,\n              }}\n            >\n              {avatar.href ? (\n                <a aria-label={avatar.alt} href={avatar.href} rel=\"noopener\">\n                  {content}\n                </a>\n              ) : (\n                content\n              )}\n            </div>\n          </motion.div>\n        );\n      })}\n\n      {hasHiddenAvatars ? (\n        <motion.div\n          animate={\n            shouldReduceMotion\n              ? {\n                  marginLeft: expanded ? 4 : -overlapPx,\n                  opacity: 1,\n                }\n              : {\n                  marginLeft: expanded ? 4 : -overlapPx,\n                  opacity: 1,\n                }\n          }\n          className=\"relative flex items-center justify-center rounded-full border-2 border-background bg-muted\"\n          style={{\n            height: size,\n            width: size,\n            zIndex: 0,\n          }}\n          transition={{\n            ...springTransition,\n            delay: shouldReduceMotion ? 0 : visibleAvatars.length * 0.03,\n          }}\n        >\n          <span\n            className=\"font-medium text-muted-foreground\"\n            style={{ fontSize: size * 0.3 }}\n          >\n            {`+${hiddenCount}`}\n          </span>\n        </motion.div>\n      ) : null}\n    </motion.div>\n  );\n};\n\nexport default AnimatedAvatarGroup;\n","path":"index.tsx","target":"components/smoothui/animated-avatar-group/index.tsx","type":"registry:ui"}],"name":"animated-avatar-group","registryDependencies":[],"title":"Animated Avatar Group","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"Animated drag-and-drop file upload component","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useCallback, useRef, useState } from \"react\";\n\nexport interface AnimatedFileUploadProps {\n  accept?: string;\n  className?: string;\n  disabled?: boolean;\n  maxSize?: number;\n  multiple?: boolean;\n  onFilesSelected: (files: File[]) => void;\n}\n\n/* ─────────────────────────────────────────────────────────\n * ANIMATION STORYBOARD\n *\n *    0ms   drop zone visible, dashed border idle\n *  drag    zone scales 1.02, border goes primary, icon floats up\n *  drop    icon bounces back, file rows slide in staggered\n * remove   file row slides out right + fades\n * layout   remaining files reorder with layout animation\n * ───────────────────────────────────────────────────────── */\n\nconst SPRING = {\n  bounce: 0.1,\n  duration: 0.25,\n  type: \"spring\" as const,\n};\n\nconst SPRING_BOUNCY = {\n  bounce: 0.2,\n  duration: 0.3,\n  type: \"spring\" as const,\n};\n\nfunction formatFileSize(bytes: number): string {\n  if (bytes < 1024) {\n    return `${bytes} B`;\n  }\n  if (bytes < 1024 * 1024) {\n    return `${(bytes / 1024).toFixed(1)} KB`;\n  }\n  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n}\n\nfunction FileIcon() {\n  return (\n    <svg\n      aria-hidden=\"true\"\n      className=\"h-4 w-4 text-muted-foreground\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth={1.5}\n      viewBox=\"0 0 24 24\"\n    >\n      <path\n        d=\"M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n    </svg>\n  );\n}\n\nfunction UploadIcon({ isDragOver }: { isDragOver: boolean }) {\n  const shouldReduceMotion = useReducedMotion();\n\n  return (\n    <motion.div\n      animate={\n        shouldReduceMotion\n          ? undefined\n          : isDragOver\n            ? { scale: 1.15, y: -4 }\n            : { scale: 1, y: 0 }\n      }\n      transition={shouldReduceMotion ? { duration: 0 } : SPRING_BOUNCY}\n    >\n      <svg\n        aria-hidden=\"true\"\n        className={cn(\n          \"mb-3 h-10 w-10 transition-colors duration-200\",\n          isDragOver ? \"text-foreground\" : \"text-muted-foreground\"\n        )}\n        fill=\"none\"\n        stroke=\"currentColor\"\n        strokeWidth={1.5}\n        viewBox=\"0 0 24 24\"\n      >\n        <path\n          d=\"M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n        />\n      </svg>\n    </motion.div>\n  );\n}\n\nexport default function AnimatedFileUpload({\n  onFilesSelected,\n  accept,\n  multiple = true,\n  maxSize,\n  className,\n  disabled = false,\n}: AnimatedFileUploadProps) {\n  const shouldReduceMotion = useReducedMotion();\n  const inputRef = useRef<HTMLInputElement>(null);\n  const [isDragOver, setIsDragOver] = useState(false);\n  const [files, setFiles] = useState<File[]>([]);\n  const [error, setError] = useState<string | null>(null);\n  const dragCounter = useRef(0);\n\n  const validateFiles = useCallback(\n    (newFiles: File[]): File[] => {\n      setError(null);\n      if (maxSize) {\n        const oversized = newFiles.filter((f) => f.size > maxSize);\n        if (oversized.length > 0) {\n          setError(\n            `${oversized.length} file(s) exceed the ${formatFileSize(maxSize)} limit`\n          );\n          return newFiles.filter((f) => f.size <= maxSize);\n        }\n      }\n      return newFiles;\n    },\n    [maxSize]\n  );\n\n  const handleFiles = useCallback(\n    (newFiles: File[]) => {\n      const valid = validateFiles(newFiles);\n      if (valid.length === 0) {\n        return;\n      }\n      const updated = multiple ? [...files, ...valid] : valid.slice(0, 1);\n      setFiles(updated);\n      onFilesSelected(updated);\n    },\n    [files, multiple, onFilesSelected, validateFiles]\n  );\n\n  const removeFile = useCallback(\n    (index: number) => {\n      const updated = files.filter((_, i) => i !== index);\n      setFiles(updated);\n      onFilesSelected(updated);\n    },\n    [files, onFilesSelected]\n  );\n\n  const handleDragEnter = useCallback(\n    (e: React.DragEvent) => {\n      e.preventDefault();\n      if (disabled) {\n        return;\n      }\n      dragCounter.current += 1;\n      setIsDragOver(true);\n    },\n    [disabled]\n  );\n\n  const handleDragLeave = useCallback((e: React.DragEvent) => {\n    e.preventDefault();\n    dragCounter.current -= 1;\n    if (dragCounter.current === 0) {\n      setIsDragOver(false);\n    }\n  }, []);\n\n  const handleDragOver = useCallback(\n    (e: React.DragEvent) => {\n      e.preventDefault();\n      if (disabled) {\n      }\n    },\n    [disabled]\n  );\n\n  const handleDrop = useCallback(\n    (e: React.DragEvent) => {\n      e.preventDefault();\n      dragCounter.current = 0;\n      setIsDragOver(false);\n      if (disabled) {\n        return;\n      }\n      const droppedFiles = Array.from(e.dataTransfer.files);\n      handleFiles(droppedFiles);\n    },\n    [disabled, handleFiles]\n  );\n\n  const handleClick = () => {\n    if (!disabled) {\n      inputRef.current?.click();\n    }\n  };\n\n  const handleKeyDown = (e: React.KeyboardEvent) => {\n    if ((e.key === \"Enter\" || e.key === \" \") && !disabled) {\n      e.preventDefault();\n      inputRef.current?.click();\n    }\n  };\n\n  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n    const selected = e.target.files ? Array.from(e.target.files) : [];\n    handleFiles(selected);\n    if (inputRef.current) {\n      inputRef.current.value = \"\";\n    }\n  };\n\n  return (\n    <div className={cn(\"w-full space-y-3\", className)}>\n      <motion.div\n        animate={\n          shouldReduceMotion\n            ? undefined\n            : isDragOver\n              ? { scale: 1.02 }\n              : { scale: 1 }\n        }\n        aria-label=\"File upload area. Drag and drop files or press to browse\"\n        className={cn(\n          \"relative flex cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed px-6 py-10\",\n          \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n          \"transition-colors duration-200\",\n          isDragOver\n            ? \"border-primary bg-primary/5\"\n            : \"border-muted-foreground/25 hover:border-muted-foreground/40 hover:bg-muted/30\",\n          disabled && \"pointer-events-none opacity-50\"\n        )}\n        onClick={handleClick}\n        onDragEnter={handleDragEnter}\n        onDragLeave={handleDragLeave}\n        onDragOver={handleDragOver}\n        onDrop={handleDrop}\n        onKeyDown={handleKeyDown}\n        role=\"button\"\n        tabIndex={disabled ? -1 : 0}\n        transition={shouldReduceMotion ? { duration: 0 } : SPRING}\n      >\n        <input\n          accept={accept}\n          className=\"sr-only\"\n          disabled={disabled}\n          multiple={multiple}\n          onChange={handleInputChange}\n          ref={inputRef}\n          type=\"file\"\n        />\n\n        <UploadIcon isDragOver={isDragOver} />\n\n        <AnimatePresence initial={false} mode=\"wait\">\n          <motion.p\n            animate={shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }}\n            className=\"font-medium text-foreground text-sm\"\n            exit={shouldReduceMotion ? { opacity: 0 } : { opacity: 0, y: -4 }}\n            initial={shouldReduceMotion ? { opacity: 0 } : { opacity: 0, y: 4 }}\n            key={isDragOver ? \"drop\" : \"idle\"}\n            transition={\n              shouldReduceMotion ? { duration: 0 } : { duration: 0.15 }\n            }\n          >\n            {isDragOver ? \"Drop files here\" : \"Drag & drop or click to upload\"}\n          </motion.p>\n        </AnimatePresence>\n        <p className=\"mt-1 text-muted-foreground text-xs\">\n          {accept ? accept.replace(/,/g, \", \") : \"Any file type\"}\n          {maxSize ? ` \\u2022 Max ${formatFileSize(maxSize)}` : null}\n        </p>\n      </motion.div>\n\n      <AnimatePresence>\n        {error ? (\n          <motion.p\n            animate={shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }}\n            className=\"text-destructive text-sm\"\n            exit={\n              shouldReduceMotion\n                ? { opacity: 0, transition: { duration: 0 } }\n                : { opacity: 0, y: -4 }\n            }\n            initial={shouldReduceMotion ? { opacity: 0 } : { opacity: 0, y: 4 }}\n            role=\"alert\"\n            transition={shouldReduceMotion ? { duration: 0 } : SPRING}\n          >\n            {error}\n          </motion.p>\n        ) : null}\n      </AnimatePresence>\n\n      {files.length > 0 && (\n        <ul aria-label=\"Selected files\" className=\"space-y-2\">\n          <AnimatePresence initial={false}>\n            {files.map((file, index) => (\n              <motion.li\n                animate={\n                  shouldReduceMotion\n                    ? { opacity: 1 }\n                    : { opacity: 1, scale: 1, x: 0 }\n                }\n                className=\"flex items-center gap-3 rounded-md border bg-muted/50 px-3 py-2\"\n                exit={\n                  shouldReduceMotion\n                    ? { opacity: 0, transition: { duration: 0 } }\n                    : {\n                        opacity: 0,\n                        scale: 0.95,\n                        transition: { duration: 0.15 },\n                        x: 24,\n                      }\n                }\n                initial={\n                  shouldReduceMotion\n                    ? { opacity: 0 }\n                    : { opacity: 0, scale: 0.95, x: -16 }\n                }\n                key={`${file.name}-${file.size}-${file.lastModified}`}\n                layout={!shouldReduceMotion}\n                transition={shouldReduceMotion ? { duration: 0 } : SPRING}\n              >\n                <FileIcon />\n                <div className=\"min-w-0 flex-1\">\n                  <p className=\"truncate font-medium text-foreground text-sm\">\n                    {file.name}\n                  </p>\n                  <p className=\"text-muted-foreground text-xs\">\n                    {formatFileSize(file.size)}\n                  </p>\n                </div>\n                <motion.button\n                  aria-label={`Remove ${file.name}`}\n                  className=\"shrink-0 rounded-md p-1 text-muted-foreground hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n                  onClick={(e) => {\n                    e.stopPropagation();\n                    removeFile(index);\n                  }}\n                  type=\"button\"\n                  whileHover={shouldReduceMotion ? undefined : { scale: 1.1 }}\n                  whileTap={shouldReduceMotion ? undefined : { scale: 0.9 }}\n                >\n                  <svg\n                    aria-hidden=\"true\"\n                    className=\"h-4 w-4\"\n                    fill=\"none\"\n                    stroke=\"currentColor\"\n                    strokeWidth={2}\n                    viewBox=\"0 0 24 24\"\n                  >\n                    <path\n                      d=\"M6 18L18 6M6 6l12 12\"\n                      strokeLinecap=\"round\"\n                      strokeLinejoin=\"round\"\n                    />\n                  </svg>\n                </motion.button>\n              </motion.li>\n            ))}\n          </AnimatePresence>\n        </ul>\n      )}\n    </div>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/animated-file-upload/index.tsx","type":"registry:ui"}],"name":"animated-file-upload","registryDependencies":[],"title":"Animated File Upload","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"A AnimatedInput component for SmoothUI.","devDependencies":[],"files":[{"content":"import { motion, useReducedMotion } from \"motion/react\";\nimport { useId, useRef, useState } from \"react\";\n\nconst EASE_IN_OUT_CUBIC_X1 = 0.4;\nconst EASE_IN_OUT_CUBIC_Y1 = 0;\nconst EASE_IN_OUT_CUBIC_X2 = 0.2;\nconst EASE_IN_OUT_CUBIC_Y2 = 1;\n\nconst LABEL_TRANSITION = {\n  duration: 0.28,\n  ease: [\n    EASE_IN_OUT_CUBIC_X1,\n    EASE_IN_OUT_CUBIC_Y1,\n    EASE_IN_OUT_CUBIC_X2,\n    EASE_IN_OUT_CUBIC_Y2,\n  ] as [number, number, number, number], // cubic-bezier tuple\n};\n\nexport interface AnimatedInputProps {\n  className?: string;\n  defaultValue?: string;\n  disabled?: boolean;\n  icon?: React.ReactNode;\n  inputClassName?: string;\n  label: string;\n  labelClassName?: string;\n  onChange?: (value: string) => void;\n  placeholder?: string;\n  value?: string;\n}\n\nexport default function AnimatedInput({\n  value,\n  defaultValue = \"\",\n  onChange,\n  label,\n  placeholder = \"\",\n  disabled = false,\n  className = \"\",\n  inputClassName = \"\",\n  labelClassName = \"\",\n  icon,\n}: AnimatedInputProps) {\n  const [internalValue, setInternalValue] = useState(defaultValue);\n  const isControlled = value !== undefined;\n  const val = isControlled ? value : internalValue;\n  const inputRef = useRef<HTMLInputElement>(null);\n  const [isFocused, setIsFocused] = useState(false);\n  const isFloating = !!val || isFocused;\n  const shouldReduceMotion = useReducedMotion();\n  const reactId = useId();\n  const inputId = `animated-input-${reactId.replace(/:/g, \"\")}`;\n\n  const getLabelAnimation = () => {\n    if (shouldReduceMotion) {\n      return {};\n    }\n    if (isFloating) {\n      return {\n        borderColor: \"var(--color-brand)\",\n        color: \"var(--color-brand)\",\n        scale: 0.85,\n        y: -24,\n      };\n    }\n    return { color: \"#6b7280\", scale: 1, y: 0 };\n  };\n\n  const getLabelStyle = () => {\n    if (!shouldReduceMotion) {\n      return {};\n    }\n    if (isFloating) {\n      return {\n        borderColor: \"var(--color-brand)\",\n        color: \"var(--color-brand)\",\n        transform: \"translateY(-24px) scale(0.85)\",\n      };\n    }\n    return {\n      color: \"#6b7280\",\n      transform: \"translateY(0) scale(1)\",\n    };\n  };\n\n  return (\n    <div className={`relative flex items-center ${className}`}>\n      {icon ? (\n        <span\n          aria-hidden=\"true\"\n          className=\"absolute top-1/2 left-3 -translate-y-1/2\"\n        >\n          {icon}\n        </span>\n      ) : null}\n      <input\n        aria-label={label}\n        className={`peer w-full rounded-sm border bg-background px-3 py-2 text-sm outline-none transition focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ${icon ? \"pl-10\" : \"\"} ${inputClassName}`}\n        disabled={disabled}\n        id={inputId}\n        onBlur={() => setIsFocused(false)}\n        onChange={(e) => {\n          if (!isControlled) {\n            setInternalValue(e.target.value);\n          }\n          onChange?.(e.target.value);\n        }}\n        onFocus={() => setIsFocused(true)}\n        placeholder={isFloating ? placeholder : \"\"}\n        ref={inputRef}\n        type=\"text\"\n        value={val}\n      />\n      <motion.label\n        animate={getLabelAnimation()}\n        className={`pointer-events-none absolute top-1/2 left-3 origin-left -translate-y-1/2 rounded-sm border border-transparent bg-background px-1 text-foreground transition-all ${labelClassName}`}\n        htmlFor={inputId}\n        style={{\n          zIndex: 2,\n          ...getLabelStyle(),\n        }}\n        transition={shouldReduceMotion ? { duration: 0 } : LABEL_TRANSITION}\n      >\n        {label}\n      </motion.label>\n    </div>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/animated-input/index.tsx","type":"registry:ui"}],"name":"animated-input","registryDependencies":["https://smoothui.dev/r/tokens.json"],"title":"Animated Input","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion","lucide-react","input-otp"],"description":"A AnimatedOTPInput component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { OTPInput, OTPInputContext } from \"input-otp\";\nimport { MinusIcon } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { type ReactNode, useContext, useEffect, useState } from \"react\";\n\n// Animation constants\nconst EASE_OUT_QUINT_X1 = 0.22;\nconst EASE_OUT_QUINT_Y1 = 1;\nconst EASE_OUT_QUINT_X2 = 0.36;\nconst EASE_OUT_QUINT_Y2 = 1;\nconst EASE_OUT_QUINT = [\n  EASE_OUT_QUINT_X1,\n  EASE_OUT_QUINT_Y1,\n  EASE_OUT_QUINT_X2,\n  EASE_OUT_QUINT_Y2,\n] as const;\nconst ANIMATION_DURATION_SHORT = 0.1;\nconst ANIMATION_DURATION_MEDIUM = 0.15;\nconst ANIMATION_DURATION_STANDARD = 0.2;\nconst ANIMATION_DURATION_LONG = 0.3;\nconst STAGGER_DELAY = 0.05;\nconst SCALE_FILLED = 1.05;\nconst SCALE_HOVER = 1.02;\nconst SCALE_TAP = 0.98;\nconst INITIAL_SCALE = 0.8;\nconst INITIAL_Y = 10;\nconst SEPARATOR_DELAY = 0.15;\n\nexport interface AnimatedInputOTPProps {\n  \"aria-describedby\"?: string;\n  \"aria-label\"?: string;\n  className?: string;\n  containerClassName?: string;\n  maxLength?: number;\n  onChange?: (value: string) => void;\n  onComplete?: (value: string) => void;\n  value?: string;\n}\n\nfunction AnimatedInputOTP({\n  className,\n  containerClassName,\n  value,\n  onChange,\n  onComplete,\n  maxLength = 6,\n  children,\n  ...props\n}: AnimatedInputOTPProps & { children: ReactNode }) {\n  const handleChange = (newValue: string) => {\n    // Only allow numeric characters\n    const numericValue = newValue.replace(/[^0-9]/g, \"\");\n    onChange?.(numericValue);\n  };\n\n  return (\n    <OTPInput\n      aria-describedby={props[\"aria-describedby\"]}\n      aria-label={props[\"aria-label\"] || \"One-time password input\"}\n      className={cn(\"disabled:cursor-not-allowed\", className)}\n      containerClassName={cn(\n        \"flex items-center gap-2 has-disabled:opacity-50\",\n        containerClassName\n      )}\n      data-slot=\"input-otp\"\n      maxLength={maxLength}\n      onChange={handleChange}\n      onComplete={onComplete}\n      value={value}\n      {...props}\n    >\n      {children}\n    </OTPInput>\n  );\n}\n\ninterface AnimatedInputOTPGroupProps {\n  children?: ReactNode;\n  className?: string;\n}\n\nfunction AnimatedInputOTPGroup({\n  className,\n  children,\n}: AnimatedInputOTPGroupProps) {\n  const shouldReduceMotion = useReducedMotion();\n  return (\n    <motion.div\n      animate={shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }}\n      className={cn(\"flex items-center gap-2\", className)}\n      data-slot=\"input-otp-group\"\n      initial={\n        shouldReduceMotion ? { opacity: 1 } : { opacity: 0, y: INITIAL_Y }\n      }\n      transition={\n        shouldReduceMotion\n          ? { duration: 0 }\n          : {\n              duration: ANIMATION_DURATION_LONG,\n              ease: EASE_OUT_QUINT,\n            }\n      }\n    >\n      {children}\n    </motion.div>\n  );\n}\n\ninterface AnimatedInputOTPSlotProps {\n  className?: string;\n  index: number;\n}\n\nfunction AnimatedInputOTPSlot({ index, className }: AnimatedInputOTPSlotProps) {\n  const inputOTPContext = useContext(OTPInputContext);\n  const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {};\n  const [isFilled, setIsFilled] = useState(false);\n  const shouldReduceMotion = useReducedMotion();\n\n  useEffect(() => {\n    if (char && !isFilled) {\n      setIsFilled(true);\n    } else if (!char && isFilled) {\n      setIsFilled(false);\n    }\n  }, [char, isFilled]);\n\n  return (\n    <motion.div\n      animate={\n        shouldReduceMotion\n          ? { opacity: 1 }\n          : {\n              opacity: 1,\n              scale: isFilled ? SCALE_FILLED : 1,\n              y: 0,\n            }\n      }\n      className={cn(\n        \"relative flex h-9 w-9 items-center justify-center rounded-md border border-zinc-300 bg-background text-sm shadow-sm outline-none transition-all aria-invalid:border-destructive data-[active=true]:z-10 data-[active=true]:border-ring data-[active=true]:ring-[3px] data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:border-destructive data-[active=true]:aria-invalid:ring-destructive/20 dark:border-zinc-700 dark:bg-input/30 dark:data-[active=true]:aria-invalid:ring-destructive/40\",\n        className\n      )}\n      data-active={isActive}\n      data-slot=\"input-otp-slot\"\n      initial={\n        shouldReduceMotion\n          ? { opacity: 1 }\n          : { opacity: 0, scale: INITIAL_SCALE, y: INITIAL_Y }\n      }\n      transition={\n        shouldReduceMotion\n          ? { duration: 0 }\n          : {\n              delay: index * STAGGER_DELAY,\n              duration: ANIMATION_DURATION_STANDARD,\n              ease: EASE_OUT_QUINT,\n              scale: {\n                duration: ANIMATION_DURATION_MEDIUM,\n                ease: EASE_OUT_QUINT,\n              },\n            }\n      }\n      whileHover={\n        shouldReduceMotion\n          ? {}\n          : {\n              scale: SCALE_HOVER,\n              transition: {\n                duration: ANIMATION_DURATION_MEDIUM,\n                ease: EASE_OUT_QUINT,\n              },\n            }\n      }\n      whileTap={\n        shouldReduceMotion\n          ? {}\n          : {\n              scale: SCALE_TAP,\n              transition: {\n                duration: ANIMATION_DURATION_SHORT,\n                ease: EASE_OUT_QUINT,\n              },\n            }\n      }\n    >\n      <AnimatePresence mode=\"wait\">\n        {char ? (\n          <motion.span\n            animate={\n              shouldReduceMotion\n                ? { opacity: 1, scale: 1 }\n                : { opacity: 1, rotateY: 0, scale: 1 }\n            }\n            className=\"font-medium\"\n            exit={\n              shouldReduceMotion\n                ? { opacity: 0, transition: { duration: 0 } }\n                : { opacity: 0, rotateY: 90, scale: 0.5 }\n            }\n            initial={\n              shouldReduceMotion\n                ? { opacity: 1, scale: 1 }\n                : { opacity: 0, rotateY: -90, scale: 0.5 }\n            }\n            key={char}\n            transition={\n              shouldReduceMotion\n                ? { duration: 0 }\n                : {\n                    duration: ANIMATION_DURATION_STANDARD,\n                    ease: EASE_OUT_QUINT,\n                  }\n            }\n          >\n            {char}\n          </motion.span>\n        ) : null}\n      </AnimatePresence>\n\n      {hasFakeCaret && !shouldReduceMotion && (\n        <motion.div\n          animate={{ opacity: 1 }}\n          className=\"pointer-events-none absolute inset-0 flex items-center justify-center\"\n          exit={{ opacity: 0 }}\n          initial={{ opacity: 0 }}\n          transition={{ duration: ANIMATION_DURATION_SHORT }}\n        >\n          <motion.div\n            animate={{ opacity: [0, 1, 0] }}\n            className=\"h-4 w-px bg-foreground\"\n            transition={{\n              duration: 1,\n              ease: [0.645, 0.045, 0.355, 1],\n              repeat: Number.POSITIVE_INFINITY,\n            }}\n          />\n        </motion.div>\n      )}\n    </motion.div>\n  );\n}\n\nfunction AnimatedInputOTPSeparator() {\n  const shouldReduceMotion = useReducedMotion();\n  return (\n    <motion.div\n      animate={shouldReduceMotion ? { opacity: 1 } : { opacity: 1, scale: 1 }}\n      data-slot=\"input-otp-separator\"\n      initial={\n        shouldReduceMotion\n          ? { opacity: 1 }\n          : { opacity: 0, scale: INITIAL_SCALE }\n      }\n      transition={\n        shouldReduceMotion\n          ? { duration: 0 }\n          : {\n              delay: SEPARATOR_DELAY,\n              duration: ANIMATION_DURATION_LONG,\n              ease: EASE_OUT_QUINT,\n            }\n      }\n    >\n      <MinusIcon className=\"h-4 w-4 text-muted-foreground\" />\n    </motion.div>\n  );\n}\n\n// Main component that combines everything\nexport function AnimatedOTPInput({\n  maxLength = 6,\n  className,\n  value,\n  onChange,\n  onComplete,\n  ...props\n}: AnimatedInputOTPProps) {\n  return (\n    <AnimatedInputOTP\n      className={className}\n      maxLength={maxLength}\n      onChange={onChange}\n      onComplete={onComplete}\n      value={value}\n      {...props}\n    >\n      <AnimatedInputOTPGroup>\n        <AnimatedInputOTPSlot index={0} />\n        <AnimatedInputOTPSlot index={1} />\n        <AnimatedInputOTPSlot index={2} />\n      </AnimatedInputOTPGroup>\n      <AnimatedInputOTPSeparator />\n      <AnimatedInputOTPGroup>\n        <AnimatedInputOTPSlot index={3} />\n        <AnimatedInputOTPSlot index={4} />\n        <AnimatedInputOTPSlot index={5} />\n      </AnimatedInputOTPGroup>\n    </AnimatedInputOTP>\n  );\n}\n\nexport {\n  AnimatedInputOTP,\n  AnimatedInputOTPGroup,\n  AnimatedInputOTPSeparator,\n  AnimatedInputOTPSlot,\n};\n\nexport default AnimatedOTPInput;\n","path":"index.tsx","target":"components/smoothui/animated-o-t-p-input/index.tsx","type":"registry:ui"}],"name":"animated-o-t-p-input","registryDependencies":[],"title":"Animated O T P Input","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"A AnimatedProgressBar component for SmoothUI.","devDependencies":[],"files":[{"content":"import { motion, useReducedMotion } from \"motion/react\";\n\nexport interface AnimatedProgressBarProps {\n  barClassName?: string;\n  className?: string;\n  color?: string;\n  label?: string;\n  labelClassName?: string;\n  value: number; // 0-100\n  /**\n   * To replay the animation, change the React 'key' prop on this component from the parent.\n   */\n}\n\nconst MIN_PROGRESS_VALUE = 0;\nconst MAX_PROGRESS_VALUE = 100;\n\nconst SPRING = {\n  damping: 10,\n  duration: 0.25,\n  mass: 0.75,\n  stiffness: 100,\n  type: \"spring\" as const,\n};\n\nexport default function AnimatedProgressBar({\n  value,\n  label,\n  color = \"#6366f1\",\n  className = \"\",\n  barClassName = \"\",\n  labelClassName = \"\",\n}: AnimatedProgressBarProps) {\n  const shouldReduceMotion = useReducedMotion();\n\n  return (\n    <div className={`w-full ${className}`}>\n      {label ? (\n        <div className={`mb-1 font-medium text-sm ${labelClassName}`}>\n          {label}\n        </div>\n      ) : null}\n      <div className=\"relative h-3 w-full overflow-hidden rounded border bg-background\">\n        <motion.div\n          animate={{\n            width: `${Math.max(MIN_PROGRESS_VALUE, Math.min(MAX_PROGRESS_VALUE, value))}%`,\n          }}\n          className={`h-full rounded bg-background ${barClassName}`}\n          initial={{ width: MIN_PROGRESS_VALUE }}\n          style={{ backgroundColor: color }}\n          transition={shouldReduceMotion ? { duration: 0 } : SPRING}\n        />\n      </div>\n    </div>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/animated-progress-bar/index.tsx","type":"registry:ui"}],"name":"animated-progress-bar","registryDependencies":[],"title":"Animated Progress Bar","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"Animated stepper/wizard component with step transitions","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { type ReactNode, useCallback, useId, useState } from \"react\";\n\nexport interface StepItem {\n  content?: ReactNode;\n  description?: string;\n  icon?: ReactNode;\n  label: string;\n}\n\nexport interface AnimatedStepperProps {\n  allowClickNavigation?: boolean;\n  className?: string;\n  currentStep?: number;\n  defaultStep?: number;\n  onStepChange?: (step: number) => void;\n  steps: StepItem[];\n  variant?: \"horizontal\" | \"vertical\";\n}\n\n/* ─────────────────────────────────────────────────────────\n * ANIMATION STORYBOARD\n *\n *    0ms   stepper enters viewport\n *  100ms   step circles stagger in (50ms each)\n *  click   active ring pulse + circle scale bounce\n *  step    progress line fills with spring\n *  done    checkmark draws with pathLength animation\n *  slide   content slides directionally with crossfade\n * ───────────────────────────────────────────────────────── */\n\nconst SPRING = {\n  bounce: 0.1,\n  duration: 0.25,\n  type: \"spring\" as const,\n};\n\nconst SPRING_BOUNCY = {\n  bounce: 0.2,\n  duration: 0.3,\n  type: \"spring\" as const,\n};\n\nfunction CheckIcon() {\n  return (\n    <svg\n      aria-hidden=\"true\"\n      className=\"h-5 w-5\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth={2.5}\n      viewBox=\"0 0 24 24\"\n    >\n      <path d=\"M5 13l4 4L19 7\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n    </svg>\n  );\n}\n\nexport default function AnimatedStepper({\n  steps,\n  currentStep: controlledStep,\n  defaultStep = 0,\n  onStepChange,\n  variant = \"horizontal\",\n  allowClickNavigation = false,\n  className,\n}: AnimatedStepperProps) {\n  const shouldReduceMotion = useReducedMotion();\n  const id = useId();\n\n  const [internalStep, setInternalStep] = useState(defaultStep);\n  const [direction, setDirection] = useState(1);\n\n  const isControlled = controlledStep !== undefined;\n  const activeStep = isControlled ? controlledStep : internalStep;\n\n  const handleStepChange = useCallback(\n    (step: number) => {\n      if (step < 0 || step >= steps.length) {\n        return;\n      }\n      setDirection(step > activeStep ? 1 : -1);\n      if (!isControlled) {\n        setInternalStep(step);\n      }\n      onStepChange?.(step);\n    },\n    [isControlled, onStepChange, activeStep, steps.length]\n  );\n\n  const handleKeyDown = useCallback(\n    (event: React.KeyboardEvent) => {\n      if (!allowClickNavigation) {\n        return;\n      }\n      const isHoriz = variant === \"horizontal\";\n      const nextKey = isHoriz ? \"ArrowRight\" : \"ArrowDown\";\n      const prevKey = isHoriz ? \"ArrowLeft\" : \"ArrowUp\";\n\n      if (event.key === nextKey) {\n        event.preventDefault();\n        handleStepChange(Math.min(activeStep + 1, steps.length - 1));\n      } else if (event.key === prevKey) {\n        event.preventDefault();\n        handleStepChange(Math.max(activeStep - 1, 0));\n      }\n    },\n    [allowClickNavigation, variant, activeStep, steps.length, handleStepChange]\n  );\n\n  const progress = steps.length > 1 ? activeStep / (steps.length - 1) : 0;\n  const isHorizontal = variant === \"horizontal\";\n\n  return (\n    <div\n      className={cn(\n        \"flex w-full gap-6\",\n        isHorizontal ? \"flex-col\" : \"flex-row\",\n        className\n      )}\n    >\n      <div\n        aria-label=\"Progress steps\"\n        className={cn(\n          \"relative flex\",\n          isHorizontal\n            ? \"flex-row items-center justify-between\"\n            : \"flex-col items-start gap-2\"\n        )}\n        role=\"group\"\n      >\n        {steps.map((step, index) => {\n          const isActive = index === activeStep;\n          const isCompleted = index < activeStep;\n\n          return (\n            <div\n              className={cn(\n                \"relative z-10 flex items-center\",\n                isHorizontal ? \"flex-1\" : \"gap-3\",\n                index < steps.length - 1 && isHorizontal && \"flex-1\"\n              )}\n              key={`${id}-step-${step.label}`}\n            >\n              <motion.button\n                animate={shouldReduceMotion ? undefined : { scale: 1 }}\n                aria-label={`Step ${index + 1}: ${step.label}${isCompleted ? \", completed\" : \"\"}${isActive ? \", current\" : \"\"}`}\n                aria-selected={isActive}\n                className={cn(\n                  \"relative flex h-10 w-10 shrink-0 items-center justify-center rounded-full border-2 font-medium text-sm\",\n                  \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n                  isActive &&\n                    \"border-primary bg-primary text-primary-foreground\",\n                  isCompleted &&\n                    \"border-primary bg-primary text-primary-foreground\",\n                  !(isActive || isCompleted) &&\n                    \"border-muted-foreground/30 bg-background text-muted-foreground\",\n                  allowClickNavigation ? \"cursor-pointer\" : \"cursor-default\"\n                )}\n                disabled={!allowClickNavigation}\n                id={`${id}-step-${index}`}\n                onClick={() => allowClickNavigation && handleStepChange(index)}\n                onKeyDown={handleKeyDown}\n                role=\"tab\"\n                tabIndex={isActive ? 0 : -1}\n                transition={shouldReduceMotion ? { duration: 0 } : SPRING}\n                type=\"button\"\n                whileTap={\n                  allowClickNavigation && !shouldReduceMotion\n                    ? { scale: 0.9 }\n                    : undefined\n                }\n              >\n                {/* Active ring pulse */}\n                {isActive && !shouldReduceMotion && (\n                  <motion.span\n                    animate={{ opacity: 0, scale: 1.6 }}\n                    className=\"absolute inset-0 rounded-full border-2 border-primary\"\n                    initial={{ opacity: 0.5, scale: 1 }}\n                    transition={{ duration: 0.6, ease: [0.23, 1, 0.32, 1] }}\n                  />\n                )}\n\n                <AnimatePresence initial={false} mode=\"wait\">\n                  {isCompleted ? (\n                    <motion.span\n                      animate={\n                        shouldReduceMotion\n                          ? { opacity: 1 }\n                          : { opacity: 1, scale: 1 }\n                      }\n                      exit={\n                        shouldReduceMotion\n                          ? { opacity: 0 }\n                          : { opacity: 0, scale: 0.5 }\n                      }\n                      initial={\n                        shouldReduceMotion\n                          ? { opacity: 0 }\n                          : { opacity: 0, scale: 0.5 }\n                      }\n                      key=\"check\"\n                      transition={\n                        shouldReduceMotion ? { duration: 0 } : SPRING_BOUNCY\n                      }\n                    >\n                      <CheckIcon />\n                    </motion.span>\n                  ) : step.icon ? (\n                    <motion.span\n                      animate={\n                        shouldReduceMotion\n                          ? { opacity: 1 }\n                          : { opacity: 1, scale: 1 }\n                      }\n                      exit={\n                        shouldReduceMotion\n                          ? { opacity: 0 }\n                          : { opacity: 0, scale: 0.8 }\n                      }\n                      initial={\n                        shouldReduceMotion\n                          ? { opacity: 0 }\n                          : { opacity: 0, scale: 0.8 }\n                      }\n                      key=\"icon\"\n                      transition={shouldReduceMotion ? { duration: 0 } : SPRING}\n                    >\n                      {step.icon}\n                    </motion.span>\n                  ) : (\n                    <motion.span\n                      animate={\n                        shouldReduceMotion\n                          ? { opacity: 1 }\n                          : { opacity: 1, scale: 1 }\n                      }\n                      exit={\n                        shouldReduceMotion\n                          ? { opacity: 0 }\n                          : { opacity: 0, scale: 0.8 }\n                      }\n                      initial={\n                        shouldReduceMotion\n                          ? { opacity: 0 }\n                          : { opacity: 0, scale: 0.8 }\n                      }\n                      key=\"number\"\n                      transition={shouldReduceMotion ? { duration: 0 } : SPRING}\n                    >\n                      {index + 1}\n                    </motion.span>\n                  )}\n                </AnimatePresence>\n              </motion.button>\n\n              {isHorizontal && (\n                <div className=\"ml-2 hidden sm:block\">\n                  <p\n                    className={cn(\n                      \"font-medium text-sm transition-colors duration-200\",\n                      isActive ? \"text-foreground\" : \"text-muted-foreground\"\n                    )}\n                  >\n                    {step.label}\n                  </p>\n                  {step.description ? (\n                    <p className=\"text-muted-foreground text-xs\">\n                      {step.description}\n                    </p>\n                  ) : null}\n                </div>\n              )}\n\n              {!isHorizontal && (\n                <div>\n                  <p\n                    className={cn(\n                      \"font-medium text-sm transition-colors duration-200\",\n                      isActive ? \"text-foreground\" : \"text-muted-foreground\"\n                    )}\n                  >\n                    {step.label}\n                  </p>\n                  {step.description ? (\n                    <p className=\"text-muted-foreground text-xs\">\n                      {step.description}\n                    </p>\n                  ) : null}\n                </div>\n              )}\n\n              {isHorizontal && index < steps.length - 1 && (\n                <div className=\"mx-2 h-0.5 flex-1 overflow-hidden rounded-full bg-muted\">\n                  <motion.div\n                    animate={{ width: index < activeStep ? \"100%\" : \"0%\" }}\n                    className=\"h-full bg-primary\"\n                    transition={shouldReduceMotion ? { duration: 0 } : SPRING}\n                  />\n                </div>\n              )}\n            </div>\n          );\n        })}\n\n        {!isHorizontal && (\n          <div className=\"absolute top-5 left-5 h-[calc(100%-2.5rem)] w-0.5 -translate-x-1/2 overflow-hidden bg-muted\">\n            <motion.div\n              animate={{ height: `${progress * 100}%` }}\n              className=\"w-full bg-primary\"\n              transition={shouldReduceMotion ? { duration: 0 } : SPRING}\n            />\n          </div>\n        )}\n      </div>\n\n      <div aria-label={`Step ${activeStep + 1} content`} role=\"tabpanel\">\n        <AnimatePresence initial={false} mode=\"wait\">\n          <motion.div\n            animate={shouldReduceMotion ? { opacity: 1 } : { opacity: 1, x: 0 }}\n            exit={\n              shouldReduceMotion\n                ? { opacity: 0, transition: { duration: 0 } }\n                : { opacity: 0, x: -direction * 20 }\n            }\n            initial={\n              shouldReduceMotion\n                ? { opacity: 0 }\n                : { opacity: 0, x: direction * 20 }\n            }\n            key={activeStep}\n            transition={shouldReduceMotion ? { duration: 0 } : SPRING}\n          >\n            {steps[activeStep]?.content}\n          </motion.div>\n        </AnimatePresence>\n      </div>\n    </div>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/animated-stepper/index.tsx","type":"registry:ui"}],"name":"animated-stepper","registryDependencies":[],"title":"Animated Stepper","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"Animated tabs component with sliding indicator","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { type ReactNode, useCallback, useId, useState } from \"react\";\n\nexport interface AnimatedTabsProps {\n  activeTab?: string;\n  className?: string;\n  defaultTab?: string;\n  layoutId?: string;\n  onChange?: (tabId: string) => void;\n  tabs: { id: string; label: string; icon?: ReactNode }[];\n  variant?: \"underline\" | \"pill\" | \"segment\";\n}\n\nconst SPRING = {\n  bounce: 0.05,\n  duration: 0.25,\n  type: \"spring\" as const,\n};\n\nexport default function AnimatedTabs({\n  tabs,\n  activeTab: controlledActiveTab,\n  defaultTab,\n  onChange,\n  variant = \"underline\",\n  layoutId: customLayoutId,\n  className,\n}: AnimatedTabsProps) {\n  const shouldReduceMotion = useReducedMotion();\n  const generatedId = useId();\n  const layoutId = customLayoutId ?? `animated-tabs-${generatedId}`;\n\n  const [internalActiveTab, setInternalActiveTab] = useState(\n    defaultTab ?? tabs[0]?.id ?? \"\"\n  );\n\n  const isControlled = controlledActiveTab !== undefined;\n  const activeTab = isControlled ? controlledActiveTab : internalActiveTab;\n\n  const handleTabChange = useCallback(\n    (tabId: string) => {\n      if (!isControlled) {\n        setInternalActiveTab(tabId);\n      }\n      onChange?.(tabId);\n    },\n    [isControlled, onChange]\n  );\n\n  const handleKeyDown = useCallback(\n    (event: React.KeyboardEvent, currentIndex: number) => {\n      let newIndex = currentIndex;\n\n      if (event.key === \"ArrowRight\") {\n        event.preventDefault();\n        newIndex = (currentIndex + 1) % tabs.length;\n      } else if (event.key === \"ArrowLeft\") {\n        event.preventDefault();\n        newIndex = (currentIndex - 1 + tabs.length) % tabs.length;\n      } else if (event.key === \"Home\") {\n        event.preventDefault();\n        newIndex = 0;\n      } else if (event.key === \"End\") {\n        event.preventDefault();\n        newIndex = tabs.length - 1;\n      } else {\n        return;\n      }\n\n      const newTab = tabs[newIndex];\n      if (newTab) {\n        handleTabChange(newTab.id);\n        const tabElement = document.getElementById(\n          `${layoutId}-tab-${newTab.id}`\n        );\n        tabElement?.focus();\n      }\n    },\n    [tabs, handleTabChange, layoutId]\n  );\n\n  const baseContainerStyles = cn(\n    \"relative inline-flex\",\n    variant === \"underline\" && \"gap-1 border-border border-b\",\n    variant === \"pill\" && \"gap-1 rounded-full bg-muted p-1\",\n    variant === \"segment\" && \"gap-0 rounded-lg bg-muted p-1\"\n  );\n\n  const getTabStyles = (isActive: boolean) =>\n    cn(\n      \"relative z-10 flex cursor-pointer items-center justify-center gap-2 px-4 py-2 font-medium text-sm transition-colors\",\n      \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n      variant === \"underline\" && [\n        \"rounded-t-md\",\n        isActive\n          ? \"text-foreground\"\n          : \"text-muted-foreground hover:text-foreground\",\n      ],\n      variant === \"pill\" && [\n        \"rounded-full\",\n        isActive\n          ? \"text-foreground\"\n          : \"text-muted-foreground hover:text-foreground\",\n      ],\n      variant === \"segment\" && [\n        \"flex-1 rounded-md\",\n        isActive\n          ? \"text-foreground\"\n          : \"text-muted-foreground hover:text-foreground\",\n      ]\n    );\n\n  const getIndicatorStyles = () =>\n    cn(\n      \"absolute\",\n      variant === \"underline\" && \"right-0 -bottom-px left-0 h-0.5 bg-brand\",\n      variant === \"pill\" &&\n        \"inset-0 rounded-full border border-border bg-background shadow-sm\",\n      variant === \"segment\" &&\n        \"inset-0 rounded-md border border-border bg-background shadow-sm\"\n    );\n\n  return (\n    <div\n      aria-label=\"Tabs\"\n      className={cn(baseContainerStyles, className)}\n      role=\"tablist\"\n    >\n      {tabs.map((tab, index) => {\n        const isActive = activeTab === tab.id;\n\n        return (\n          <button\n            aria-selected={isActive}\n            className={getTabStyles(isActive)}\n            id={`${layoutId}-tab-${tab.id}`}\n            key={tab.id}\n            onClick={() => handleTabChange(tab.id)}\n            onKeyDown={(e) => handleKeyDown(e, index)}\n            role=\"tab\"\n            tabIndex={isActive ? 0 : -1}\n            type=\"button\"\n          >\n            {isActive && (\n              <motion.span\n                className={getIndicatorStyles()}\n                layout\n                layoutId={layoutId}\n                style={{ originY: \"0px\" }}\n                transition={shouldReduceMotion ? { duration: 0 } : SPRING}\n              />\n            )}\n            {tab.icon ? (\n              <span className=\"relative z-10\">{tab.icon}</span>\n            ) : null}\n            <span className=\"relative z-10\">{tab.label}</span>\n          </button>\n        );\n      })}\n    </div>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/animated-tabs/index.tsx","type":"registry:ui"}],"name":"animated-tabs","registryDependencies":["https://smoothui.dev/r/tokens.json"],"title":"Animated Tabs","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion","lucide-react"],"description":"A AnimatedTags component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { CircleX, Plus } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useState } from \"react\";\n\nexport interface AnimatedTagsProps {\n  className?: string;\n  initialTags?: string[];\n  onChange?: (selected: string[]) => void;\n  selectedTags?: string[];\n}\n\nexport default function AnimatedTags({\n  initialTags = [\"react\", \"tailwindcss\", \"javascript\"],\n  selectedTags: controlledSelectedTags,\n  onChange,\n  className = \"\",\n}: AnimatedTagsProps) {\n  const [internalSelected, setInternalSelected] = useState<string[]>([]);\n  const shouldReduceMotion = useReducedMotion();\n\n  const selectedTag = controlledSelectedTags ?? internalSelected;\n  const tags = initialTags.filter((tag) => !selectedTag.includes(tag));\n\n  const handleTagClick = (tag: string) => {\n    const newSelected = [...selectedTag, tag];\n    if (onChange) {\n      onChange(newSelected);\n    } else {\n      setInternalSelected(newSelected);\n    }\n  };\n  const handleDeleteTag = (tag: string) => {\n    const newSelectedTag = selectedTag.filter((selected) => selected !== tag);\n    if (onChange) {\n      onChange(newSelectedTag);\n    } else {\n      setInternalSelected(newSelectedTag);\n    }\n  };\n  return (\n    <div className={`flex w-[300px] flex-col gap-4 p-4 ${className}`}>\n      <div className=\"flex flex-col items-start justify-center gap-1\">\n        <p>Selected Tags</p>\n        <AnimatePresence>\n          <div className=\"flex min-h-12 w-full flex-wrap items-center gap-1 rounded-xl border bg-background p-2\">\n            {selectedTag?.map((tag) => (\n              <motion.div\n                animate={\n                  shouldReduceMotion\n                    ? { opacity: 1 }\n                    : {\n                        filter: \"blur(0px)\",\n                        opacity: 1,\n                        y: 0,\n                      }\n                }\n                className=\"group flex cursor-pointer flex-row items-center justify-center gap-2 rounded-md border bg-primary px-2 py-1 text-primary-foreground group-hover:bg-primary group-hover:text-foreground\"\n                exit={\n                  shouldReduceMotion\n                    ? { opacity: 0, transition: { duration: 0 } }\n                    : { filter: \"blur(4px)\", opacity: 0, y: 20 }\n                }\n                initial={\n                  shouldReduceMotion\n                    ? { opacity: 1 }\n                    : { filter: \"blur(4px)\", opacity: 0, y: 20 }\n                }\n                key={tag}\n                layout\n                onClick={() => handleDeleteTag(tag)}\n                transition={\n                  shouldReduceMotion\n                    ? { duration: 0 }\n                    : { bounce: 0, duration: 0.25, type: \"spring\" as const }\n                }\n              >\n                {tag}{\" \"}\n                <CircleX\n                  className=\"ease flex items-center justify-center rounded-full transition-all duration-200\"\n                  size={16}\n                />\n              </motion.div>\n            ))}\n          </div>\n        </AnimatePresence>\n      </div>\n      <AnimatePresence>\n        <div className=\"flex flex-wrap items-center gap-1\">\n          {tags.map((tag) => (\n            <motion.div\n              animate={\n                shouldReduceMotion\n                  ? { opacity: 1 }\n                  : {\n                      filter: \"blur(0px)\",\n                      opacity: 1,\n                      y: 0,\n                    }\n              }\n              className=\"group flex cursor-pointer flex-row items-center justify-center gap-2 rounded-md border bg-background px-2 py-1 text-primary-foreground\"\n              exit={\n                shouldReduceMotion\n                  ? { opacity: 0, transition: { duration: 0 } }\n                  : { filter: \"blur(4px)\", opacity: 0, y: -20 }\n              }\n              initial={\n                shouldReduceMotion\n                  ? { opacity: 1 }\n                  : { filter: \"blur(4px)\", opacity: 0, y: -20 }\n              }\n              key={tag}\n              layout\n              onClick={() => handleTagClick(tag)}\n              transition={\n                shouldReduceMotion\n                  ? { duration: 0 }\n                  : { bounce: 0, duration: 0.25, type: \"spring\" as const }\n              }\n            >\n              {tag}{\" \"}\n              <Plus\n                className=\"ease @media (hover: hover) and (pointer: flex items-center justify-center rounded-full transition-all duration-200 fine):hover:bg-primary group-hover:text-foreground\"\n                size={16}\n              />\n            </motion.div>\n          ))}\n        </div>\n      </AnimatePresence>\n    </div>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/animated-tags/index.tsx","type":"registry:ui"}],"name":"animated-tags","registryDependencies":[],"title":"Animated Tags","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"Animated toggle switch with morph and icon variants","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport {\n  type KeyboardEvent,\n  type ReactNode,\n  useCallback,\n  useState,\n} from \"react\";\n\nexport interface AnimatedToggleProps {\n  /** Controlled checked state */\n  checked?: boolean;\n  /** Additional CSS classes */\n  className?: string;\n  /** Default checked state for uncontrolled mode */\n  defaultChecked?: boolean;\n  /** Whether the toggle is disabled */\n  disabled?: boolean;\n  /** Icons for on/off states (only used with icon variant) */\n  icons?: { on: ReactNode; off: ReactNode };\n  /** Accessible label for the toggle */\n  label?: string;\n  /** Callback when checked state changes */\n  onChange?: (checked: boolean) => void;\n  /** Size of the toggle */\n  size?: \"sm\" | \"md\" | \"lg\";\n  /** Visual variant of the toggle */\n  variant?: \"default\" | \"morph\" | \"icon\";\n}\n\nconst SPRING = {\n  bounce: 0.1,\n  duration: 0.25,\n  type: \"spring\" as const,\n};\n\nconst SIZES = {\n  lg: {\n    icon: \"size-3.5\",\n    thumb: \"size-6\",\n    thumbTranslate: 24,\n    track: \"w-[52px] h-7\",\n  },\n  md: {\n    icon: \"size-3\",\n    thumb: \"size-5\",\n    thumbTranslate: 20,\n    track: \"w-11 h-6\",\n  },\n  sm: {\n    icon: \"size-2.5\",\n    thumb: \"size-4\",\n    thumbTranslate: 16,\n    track: \"w-9 h-5\",\n  },\n};\n\nconst AnimatedToggle = ({\n  checked: controlledChecked,\n  defaultChecked = false,\n  onChange,\n  variant = \"default\",\n  icons,\n  size = \"md\",\n  disabled = false,\n  label,\n  className,\n}: AnimatedToggleProps) => {\n  const shouldReduceMotion = useReducedMotion();\n\n  const [internalChecked, setInternalChecked] = useState(defaultChecked);\n\n  const isControlled = controlledChecked !== undefined;\n  const checked = isControlled ? controlledChecked : internalChecked;\n\n  const handleToggle = useCallback(() => {\n    if (disabled) {\n      return;\n    }\n\n    const newValue = !checked;\n    if (!isControlled) {\n      setInternalChecked(newValue);\n    }\n    onChange?.(newValue);\n  }, [checked, disabled, isControlled, onChange]);\n\n  const handleKeyDown = useCallback(\n    (event: KeyboardEvent<HTMLButtonElement>) => {\n      if (event.key === \" \" || event.key === \"Enter\") {\n        event.preventDefault();\n        handleToggle();\n      }\n    },\n    [handleToggle]\n  );\n\n  const sizeConfig = SIZES[size];\n\n  const getThumbBorderRadius = () => {\n    if (variant !== \"morph\" || shouldReduceMotion) {\n      return 9999;\n    }\n    return checked ? 9999 : 6;\n  };\n\n  const getThumbTransform = () => {\n    const translateX = checked ? sizeConfig.thumbTranslate : 0;\n    return translateX;\n  };\n\n  return (\n    <button\n      aria-checked={checked}\n      aria-label={label}\n      className={cn(\n        \"relative inline-flex shrink-0 cursor-pointer items-center rounded-full p-0.5 transition-colors\",\n        \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n        checked ? \"bg-brand\" : \"bg-muted-foreground/30\",\n        disabled && \"cursor-not-allowed opacity-50\",\n        sizeConfig.track,\n        className\n      )}\n      disabled={disabled}\n      onClick={handleToggle}\n      onKeyDown={handleKeyDown}\n      role=\"switch\"\n      type=\"button\"\n    >\n      <motion.span\n        animate={\n          shouldReduceMotion\n            ? {\n                x: getThumbTransform(),\n              }\n            : {\n                borderRadius: getThumbBorderRadius(),\n                x: getThumbTransform(),\n              }\n        }\n        className={cn(\n          \"pointer-events-none flex items-center justify-center rounded-full border border-border bg-background shadow-sm\",\n          sizeConfig.thumb\n        )}\n        initial={false}\n        style={{\n          borderRadius: getThumbBorderRadius(),\n        }}\n        transition={shouldReduceMotion ? { duration: 0 } : SPRING}\n      >\n        {variant === \"icon\" && icons && (\n          <AnimatePresence initial={false} mode=\"wait\">\n            <motion.span\n              animate={\n                shouldReduceMotion\n                  ? { opacity: 1 }\n                  : { opacity: 1, rotate: 0, scale: 1 }\n              }\n              className={cn(\n                \"flex items-center justify-center text-muted-foreground\",\n                sizeConfig.icon\n              )}\n              exit={\n                shouldReduceMotion\n                  ? { opacity: 0, transition: { duration: 0 } }\n                  : { opacity: 0, rotate: -90, scale: 0.5 }\n              }\n              initial={\n                shouldReduceMotion\n                  ? { opacity: 0 }\n                  : { opacity: 0, rotate: 90, scale: 0.5 }\n              }\n              key={checked ? \"on\" : \"off\"}\n              transition={shouldReduceMotion ? { duration: 0 } : SPRING}\n            >\n              {checked ? icons.on : icons.off}\n            </motion.span>\n          </AnimatePresence>\n        )}\n      </motion.span>\n    </button>\n  );\n};\n\nexport default AnimatedToggle;\n","path":"index.tsx","target":"components/smoothui/animated-toggle/index.tsx","type":"registry:ui"}],"name":"animated-toggle","registryDependencies":["https://smoothui.dev/r/tokens.json"],"title":"Animated Toggle","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"Spring-animated tooltip with multiple placement options","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport type { ReactNode } from \"react\";\nimport { useCallback, useEffect, useId, useRef, useState } from \"react\";\n\nexport type AnimatedTooltipPlacement = \"top\" | \"bottom\" | \"left\" | \"right\";\n\nexport interface AnimatedTooltipProps {\n  /** The trigger element the tooltip is anchored to */\n  children: ReactNode;\n  /** Additional CSS class names for the tooltip container */\n  className?: string;\n  /** Content displayed inside the tooltip */\n  content: ReactNode;\n  /** Delay in milliseconds before the tooltip appears */\n  delay?: number;\n  /** Placement of the tooltip relative to the trigger */\n  placement?: AnimatedTooltipPlacement;\n}\n\nconst SPRING = {\n  bounce: 0.1,\n  duration: 0.25,\n  type: \"spring\" as const,\n};\n\nconst placementStyles: Record<AnimatedTooltipPlacement, string> = {\n  bottom: \"top-full left-1/2 -translate-x-1/2 mt-2\",\n  left: \"right-full top-1/2 -translate-y-1/2 mr-2\",\n  right: \"left-full top-1/2 -translate-y-1/2 ml-2\",\n  top: \"bottom-full left-1/2 -translate-x-1/2 mb-2\",\n};\n\nconst arrowStyles: Record<AnimatedTooltipPlacement, string> = {\n  bottom:\n    \"bottom-full left-1/2 -translate-x-1/2 border-b-foreground border-x-transparent border-t-transparent\",\n  left: \"left-full top-1/2 -translate-y-1/2 border-l-foreground border-y-transparent border-r-transparent\",\n  right:\n    \"right-full top-1/2 -translate-y-1/2 border-r-foreground border-y-transparent border-l-transparent\",\n  top: \"top-full left-1/2 -translate-x-1/2 border-t-foreground border-x-transparent border-b-transparent\",\n};\n\nconst arrowBorderSize: Record<AnimatedTooltipPlacement, string> = {\n  bottom: \"border-4\",\n  left: \"border-4\",\n  right: \"border-4\",\n  top: \"border-4\",\n};\n\nconst getInitialTransform = (\n  placement: AnimatedTooltipPlacement\n): { opacity: number; scale: number; x: number; y: number } => {\n  const base = { opacity: 0, scale: 0.95, x: 0, y: 0 };\n  switch (placement) {\n    case \"top\":\n      return { ...base, y: 4 };\n    case \"bottom\":\n      return { ...base, y: -4 };\n    case \"left\":\n      return { ...base, x: 4 };\n    case \"right\":\n      return { ...base, x: -4 };\n  }\n};\n\nconst AnimatedTooltip = ({\n  content,\n  placement = \"top\",\n  delay = 0,\n  children,\n  className,\n}: AnimatedTooltipProps) => {\n  const shouldReduceMotion = useReducedMotion();\n  const [isVisible, setIsVisible] = useState(false);\n  const [isHoverDevice, setIsHoverDevice] = useState(false);\n  const tooltipId = useId();\n  const delayTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n  useEffect(() => {\n    const mediaQuery = window.matchMedia(\"(hover: hover) and (pointer: fine)\");\n    setIsHoverDevice(mediaQuery.matches);\n\n    const handleChange = (e: MediaQueryListEvent) => {\n      setIsHoverDevice(e.matches);\n    };\n\n    mediaQuery.addEventListener(\"change\", handleChange);\n    return () => mediaQuery.removeEventListener(\"change\", handleChange);\n  }, []);\n\n  const show = useCallback(() => {\n    if (delay > 0) {\n      delayTimerRef.current = setTimeout(() => {\n        setIsVisible(true);\n      }, delay);\n    } else {\n      setIsVisible(true);\n    }\n  }, [delay]);\n\n  const hide = useCallback(() => {\n    if (delayTimerRef.current !== null) {\n      clearTimeout(delayTimerRef.current);\n      delayTimerRef.current = null;\n    }\n    setIsVisible(false);\n  }, []);\n\n  const handleKeyDown = useCallback(\n    (event: React.KeyboardEvent) => {\n      if (event.key === \"Escape\") {\n        hide();\n      }\n    },\n    [hide]\n  );\n\n  const initialTransform = getInitialTransform(placement);\n\n  return (\n    <span\n      className=\"relative inline-flex\"\n      onBlur={hide}\n      onFocus={show}\n      onKeyDown={handleKeyDown}\n      onMouseEnter={isHoverDevice ? show : undefined}\n      onMouseLeave={isHoverDevice ? hide : undefined}\n    >\n      <span aria-describedby={isVisible ? tooltipId : undefined}>\n        {children}\n      </span>\n\n      <AnimatePresence>\n        {isVisible ? (\n          <motion.span\n            animate={\n              shouldReduceMotion\n                ? { opacity: 1 }\n                : { opacity: 1, scale: 1, x: 0, y: 0 }\n            }\n            className={cn(\n              \"absolute z-50 w-max max-w-xs rounded-md bg-foreground px-3 py-1.5 text-background text-sm shadow-md\",\n              placementStyles[placement],\n              className\n            )}\n            exit={\n              shouldReduceMotion\n                ? { opacity: 0, transition: { duration: 0 } }\n                : {\n                    ...initialTransform,\n                    transition: { duration: 0.15 },\n                  }\n            }\n            id={tooltipId}\n            initial={shouldReduceMotion ? { opacity: 0 } : initialTransform}\n            role=\"tooltip\"\n            transition={shouldReduceMotion ? { duration: 0 } : SPRING}\n          >\n            {content}\n            <span\n              aria-hidden=\"true\"\n              className={cn(\n                \"absolute block h-0 w-0\",\n                arrowBorderSize[placement],\n                arrowStyles[placement]\n              )}\n            />\n          </motion.span>\n        ) : null}\n      </AnimatePresence>\n    </span>\n  );\n};\n\nexport default AnimatedTooltip;\n","path":"index.tsx","target":"components/smoothui/animated-tooltip/index.tsx","type":"registry:ui"}],"name":"animated-tooltip","registryDependencies":[],"title":"Animated Tooltip","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"Persistent WebGL shader transition wrapper that swaps content behind a cinematic aperture bloom.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { useReducedMotion } from \"motion/react\";\nimport { type ReactNode, useEffect, useRef, useState } from \"react\";\n\nexport interface ApertureBlurTransitionProps {\n  children: ReactNode;\n  className?: string;\n  duration?: number;\n  onRest?: () => void;\n  transitionKey: string | number;\n}\n\nconst DEFAULT_DURATION_MS = 760;\nconst MIDPOINT = 0.48;\nconst MAX_DPR = 2;\nconst VERTEX_SHADER =\n  \"attribute vec2 a; void main(){ gl_Position = vec4(a, 0.0, 1.0); }\";\nconst FRAGMENT_SHADER = `\nprecision mediump float;\nuniform vec2 uRes;\nuniform float uTime;\nuniform float uProgress;\nuniform float uAlpha;\n#define PI 3.14159265359\n\nfloat hash(vec2 p){ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }\n\nvoid main(){\n  vec2 uv = gl_FragCoord.xy / uRes;\n  vec2 aspect = vec2(uRes.x / uRes.y, 1.0);\n  vec2 p = (uv - 0.5) * aspect;\n  float r = length(p);\n  float a = atan(p.y, p.x);\n\n  float progress = smoothstep(0.0, 1.0, uProgress);\n  float radius = mix(0.05, 0.82, progress);\n  float apertureNoise = sin(a * 8.0 + uTime * 0.9) * 0.018 + sin(a * 17.0 - uTime * 0.6) * 0.006;\n  float d = r - radius - apertureNoise;\n\n  float exitFade = smoothstep(0.94, 0.58, progress);\n  float ring = exp(-d * d * 210.0) * exitFade;\n  float innerGlow = smoothstep(radius + 0.18, radius - 0.02, r) * smoothstep(0.0, 0.34, progress) * exitFade;\n  float outerFeather = smoothstep(radius + 0.22, radius - 0.04, r) * exitFade * 0.55;\n  float grain = hash(floor(uv * uRes / 3.0) + floor(uTime * 18.0)) * 0.045;\n\n  vec3 champagne = vec3(1.0, 0.82, 0.56);\n  vec3 rose = vec3(1.0, 0.34, 0.62);\n  vec3 cyan = vec3(0.62, 0.92, 1.0);\n  vec3 color = mix(champagne, rose, smoothstep(-0.4, 0.4, sin(a * 2.0 + uTime * 0.7)));\n  color = mix(color, cyan, ring * 0.38 + smoothstep(0.74, 1.0, progress) * 0.18);\n\n  float highlight = ring * (0.82 + grain) + innerGlow * 0.18 + outerFeather * 0.08;\n  float alpha = clamp(highlight * uAlpha, 0.0, 0.88);\n  gl_FragColor = vec4(color * alpha + vec3(1.0) * ring * 0.22 * uAlpha, alpha);\n}\n`;\nconst STRIP = new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]);\n\ninterface Playback {\n  cancel: () => void;\n}\n\ninterface Controller {\n  destroy: () => void;\n  getProgress: () => number;\n  setAlpha: (alpha: number) => void;\n  setProgress: (progress: number) => void;\n}\n\nconst clamp01 = (value: number) => Math.max(0, Math.min(1, value));\nconst easeOutCubic = (value: number) => 1 - (1 - value) ** 3;\nconst easeInOutCubic = (value: number) =>\n  value < 0.5 ? 4 * value ** 3 : 1 - (-2 * value + 2) ** 3 / 2;\n\nconst compile = (gl: WebGLRenderingContext, type: number, source: string) => {\n  const shader = gl.createShader(type);\n  if (!shader) {\n    return null;\n  }\n  gl.shaderSource(shader, source);\n  gl.compileShader(shader);\n  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n    gl.deleteShader(shader);\n    return null;\n  }\n  return shader;\n};\n\nconst resize = (canvas: HTMLCanvasElement, gl: WebGLRenderingContext) => {\n  const rect = canvas.getBoundingClientRect();\n  const dpr = Math.min(window.devicePixelRatio || 1, MAX_DPR);\n  const width = Math.max(1, Math.round(rect.width * dpr));\n  const height = Math.max(1, Math.round(rect.height * dpr));\n  if (canvas.width !== width || canvas.height !== height) {\n    canvas.width = width;\n    canvas.height = height;\n  }\n  gl.viewport(0, 0, width, height);\n};\n\nconst createController = (canvas: HTMLCanvasElement): Controller | null => {\n  const gl = canvas.getContext(\"webgl\", {\n    alpha: true,\n    antialias: false,\n    premultipliedAlpha: true,\n  });\n  if (!gl) {\n    return null;\n  }\n  const vertex = compile(gl, gl.VERTEX_SHADER, VERTEX_SHADER);\n  const fragment = compile(gl, gl.FRAGMENT_SHADER, FRAGMENT_SHADER);\n  const program = gl.createProgram();\n  if (!(vertex && fragment && program)) {\n    return null;\n  }\n  gl.attachShader(program, vertex);\n  gl.attachShader(program, fragment);\n  gl.linkProgram(program);\n  gl.deleteShader(vertex);\n  gl.deleteShader(fragment);\n  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n    gl.deleteProgram(program);\n    return null;\n  }\n\n  // biome-ignore lint/correctness/useHookAtTopLevel: WebGLRenderingContext.useProgram is not a React hook.\n  gl.useProgram(program);\n  const buffer = gl.createBuffer();\n  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);\n  gl.bufferData(gl.ARRAY_BUFFER, STRIP, gl.STATIC_DRAW);\n  const position = gl.getAttribLocation(program, \"a\");\n  gl.enableVertexAttribArray(position);\n  gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0);\n  gl.enable(gl.BLEND);\n  gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);\n\n  const uRes = gl.getUniformLocation(program, \"uRes\");\n  const uTime = gl.getUniformLocation(program, \"uTime\");\n  const uProgress = gl.getUniformLocation(program, \"uProgress\");\n  const uAlpha = gl.getUniformLocation(program, \"uAlpha\");\n  const startedAt = performance.now();\n  let progress = 0;\n  let alpha = 0;\n  let frame = 0;\n  let destroyed = false;\n\n  const tick = () => {\n    if (destroyed) {\n      return;\n    }\n    resize(canvas, gl);\n    gl.clearColor(0, 0, 0, 0);\n    gl.clear(gl.COLOR_BUFFER_BIT);\n    gl.uniform2f(uRes, canvas.width, canvas.height);\n    gl.uniform1f(uTime, (performance.now() - startedAt) / 1000);\n    gl.uniform1f(uProgress, progress);\n    gl.uniform1f(uAlpha, alpha);\n    gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);\n    frame = requestAnimationFrame(tick);\n  };\n  frame = requestAnimationFrame(tick);\n\n  return {\n    destroy: () => {\n      destroyed = true;\n      cancelAnimationFrame(frame);\n      gl.clear(gl.COLOR_BUFFER_BIT);\n      if (buffer) {\n        gl.deleteBuffer(buffer);\n      }\n      gl.deleteProgram(program);\n    },\n    getProgress: () => progress,\n    setAlpha: (nextAlpha: number) => {\n      alpha = clamp01(nextAlpha);\n    },\n    setProgress: (nextProgress: number) => {\n      progress = clamp01(nextProgress);\n    },\n  };\n};\n\nconst play = (\n  controller: Controller,\n  duration: number,\n  onMidpoint: () => void,\n  onComplete: () => void\n): Playback => {\n  let frame = 0;\n  let cancelled = false;\n  let didSwap = false;\n  const startedAt = performance.now();\n  controller.setAlpha(1);\n\n  const advance = () => {\n    if (cancelled) {\n      return;\n    }\n    const raw = clamp01((performance.now() - startedAt) / duration);\n    const progress = easeOutCubic(raw);\n    controller.setProgress(progress);\n    if (!(didSwap || progress < MIDPOINT)) {\n      didSwap = true;\n      onMidpoint();\n    }\n    if (raw < 1) {\n      frame = requestAnimationFrame(advance);\n      return;\n    }\n    if (!didSwap) {\n      onMidpoint();\n    }\n    const fadeStartedAt = performance.now();\n    const fade = () => {\n      if (cancelled) {\n        return;\n      }\n      const fadeRaw = clamp01((performance.now() - fadeStartedAt) / 70);\n      controller.setAlpha(1 - easeInOutCubic(fadeRaw));\n      if (fadeRaw < 1) {\n        frame = requestAnimationFrame(fade);\n        return;\n      }\n      controller.setAlpha(0);\n      controller.setProgress(0);\n      onComplete();\n    };\n    frame = requestAnimationFrame(fade);\n  };\n  frame = requestAnimationFrame(advance);\n\n  return {\n    cancel: () => {\n      cancelled = true;\n      cancelAnimationFrame(frame);\n    },\n  };\n};\n\nconst ApertureBlurTransition = ({\n  children,\n  className,\n  duration = DEFAULT_DURATION_MS,\n  onRest,\n  transitionKey,\n}: ApertureBlurTransitionProps) => {\n  const shouldReduceMotion = useReducedMotion();\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const controllerRef = useRef<Controller | null>(null);\n  const playbackRef = useRef<Playback | null>(null);\n  const previousKey = useRef(transitionKey);\n  const [renderedChildren, setRenderedChildren] = useState(children);\n  const [isActive, setIsActive] = useState(false);\n\n  useEffect(() => {\n    const canvas = canvasRef.current;\n    if (!canvas) {\n      return;\n    }\n    const controller = createController(canvas);\n    controllerRef.current = controller;\n    return () => {\n      playbackRef.current?.cancel();\n      controller?.destroy();\n      controllerRef.current = null;\n    };\n  }, []);\n\n  useEffect(() => {\n    if (previousKey.current === transitionKey) {\n      setRenderedChildren(children);\n      return;\n    }\n    previousKey.current = transitionKey;\n    playbackRef.current?.cancel();\n\n    if (shouldReduceMotion) {\n      setRenderedChildren(children);\n      setIsActive(false);\n      onRest?.();\n      return;\n    }\n\n    const controller = controllerRef.current;\n    if (!controller) {\n      setRenderedChildren(children);\n      onRest?.();\n      return;\n    }\n    setIsActive(true);\n    playbackRef.current = play(\n      controller,\n      duration,\n      () => setRenderedChildren(children),\n      () => {\n        setIsActive(false);\n        playbackRef.current = null;\n        onRest?.();\n      }\n    );\n  }, [children, duration, onRest, shouldReduceMotion, transitionKey]);\n\n  return (\n    <div\n      className={cn(\n        \"relative isolate overflow-hidden rounded-2xl bg-background text-foreground\",\n        className\n      )}\n      data-transitioning={isActive ? \"true\" : \"false\"}\n    >\n      <div className=\"relative z-0\">{renderedChildren}</div>\n      <canvas\n        className={cn(\n          \"pointer-events-none absolute inset-0 z-10 h-full w-full transition-opacity duration-75\",\n          isActive && !shouldReduceMotion ? \"opacity-100\" : \"opacity-0\"\n        )}\n        ref={canvasRef}\n      />\n    </div>\n  );\n};\n\nexport default ApertureBlurTransition;\n","path":"index.tsx","target":"components/smoothui/aperture-blur-transition/index.tsx","type":"registry:ui"}],"name":"aperture-blur-transition","registryDependencies":[],"title":"Aperture Blur Transition","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion","lucide-react"],"description":"A AppDownloadStack component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { ChevronDown } from \"lucide-react\";\nimport {\n  AnimatePresence,\n  motion,\n  useAnimation,\n  useReducedMotion,\n} from \"motion/react\";\nimport { useCallback, useMemo, useState } from \"react\";\n\nexport interface AppData {\n  icon: string;\n  id: number;\n  name: string;\n}\n\nexport interface AppDownloadStackProps {\n  apps?: AppData[];\n  className?: string;\n  isExpanded?: boolean;\n  onChange?: (selected: number[]) => void;\n  onDownload?: (selected: number[]) => void;\n  onExpandChange?: (expanded: boolean) => void;\n  selectedApps?: number[];\n  title?: string;\n}\n\nconst DOWNLOAD_DURATION_MS = 3000;\nconst RESET_DELAY_MS = 1000;\nconst ROTATION_MULTIPLIER = 8;\nconst TRANSLATION_MULTIPLIER = 3;\nconst BASE_Z_INDEX = 40;\nconst Z_INDEX_STEP = 10;\nconst HOVER_X_MULTIPLIER = 10;\nconst HOVER_Y_MULTIPLIER = 10;\nconst FLOAT_AMPLITUDE = 5;\nconst FLOAT_DURATION = 2;\nconst FLOAT_DELAY_MULTIPLIER = 0.2;\nconst STAGGER_DELAY_MULTIPLIER = 0.1;\nconst TRANSITION_DURATION = 0.3;\nconst CHECKMARK_TRANSITION_DURATION = 0.3;\n\nconst defaultApps: AppData[] = [\n  {\n    icon: \"https://parsefiles.back4app.com/JPaQcFfEEQ1ePBxbf6wvzkPMEqKYHhPYv8boI1Rc/9c9721583ecba33e59ebcebdca2248fd_Mmr12FRh5V.png\",\n    id: 1,\n    name: \"GitHub\",\n  },\n  {\n    icon: \"https://parsefiles.back4app.com/JPaQcFfEEQ1ePBxbf6wvzkPMEqKYHhPYv8boI1Rc/b47f43e02f04563447fa90d4ff6c8943_9KzW5GTggQ.png\",\n    id: 2,\n    name: \"Canary\",\n  },\n  {\n    icon: \"https://parsefiles.back4app.com/JPaQcFfEEQ1ePBxbf6wvzkPMEqKYHhPYv8boI1Rc/f0b9cdefa67b57eeb080278c2f6984cc_sCqUJBg6Qq.png\",\n    id: 3,\n    name: \"Figma\",\n  },\n  {\n    icon: \"https://parsefiles.back4app.com/JPaQcFfEEQ1ePBxbf6wvzkPMEqKYHhPYv8boI1Rc/178c7b02003c933e6b5afe98bbee595b_low_res_Arc_Browser.png\",\n    id: 4,\n    name: \"Arc\",\n  },\n];\n\nexport default function AppDownloadStack({\n  apps = defaultApps,\n  title = \"Starter Mac\",\n  selectedApps: controlledSelected,\n  onChange,\n  onDownload,\n  isExpanded: controlledExpanded,\n  onExpandChange,\n  className = \"\",\n}: AppDownloadStackProps) {\n  const [internalExpanded, setInternalExpanded] = useState(false);\n  const [internalSelected, setInternalSelected] = useState<number[]>([]);\n  const [isDownloading, setIsDownloading] = useState(false);\n  const [downloadComplete, setDownloadComplete] = useState(false);\n  const shineControls = useAnimation();\n  const shouldReduceMotion = useReducedMotion();\n\n  const isExpanded =\n    controlledExpanded === undefined ? internalExpanded : controlledExpanded;\n  const selected = controlledSelected ?? internalSelected;\n\n  const setExpanded = (val: boolean) => {\n    if (onExpandChange) {\n      onExpandChange(val);\n    } else {\n      setInternalExpanded(val);\n    }\n  };\n\n  const toggleApp = useCallback(\n    (id: number) => {\n      const newSelected = selected.includes(id)\n        ? selected.filter((appId) => appId !== id)\n        : [...selected, id];\n      if (onChange) {\n        onChange(newSelected);\n      } else {\n        setInternalSelected(newSelected);\n      }\n    },\n    [selected, onChange]\n  );\n\n  const handleDownload = useCallback(() => {\n    setIsDownloading(true);\n    if (onDownload) {\n      onDownload(selected);\n    }\n    if (!shouldReduceMotion) {\n      shineControls.start({\n        transition: {\n          duration: 1,\n          ease: \"linear\",\n          repeat: Number.POSITIVE_INFINITY,\n        },\n        x: [\"0%\", \"100%\"],\n      });\n    }\n    setTimeout(() => {\n      shineControls.stop();\n      setDownloadComplete(true);\n      setTimeout(() => {\n        if (onExpandChange) {\n          onExpandChange(false);\n        } else {\n          setInternalExpanded(false);\n        }\n        if (onChange) {\n          onChange([]);\n        } else {\n          setInternalSelected([]);\n        }\n        setIsDownloading(false);\n        setDownloadComplete(false);\n      }, RESET_DELAY_MS);\n    }, DOWNLOAD_DURATION_MS);\n  }, [\n    shineControls,\n    selected,\n    onDownload,\n    onChange,\n    onExpandChange,\n    shouldReduceMotion,\n  ]);\n\n  const stackVariants = useMemo(\n    // biome-ignore lint/suspicious/noExplicitAny: Variants type requires flexible return\n    (): Record<string, (i: number) => any> => ({\n      float: (i: number) =>\n        shouldReduceMotion\n          ? { y: 0 }\n          : {\n              transition: {\n                y: {\n                  delay: i * FLOAT_DELAY_MULTIPLIER,\n                  duration: FLOAT_DURATION,\n                  ease: [0.645, 0.045, 0.355, 1],\n                  repeat: Number.POSITIVE_INFINITY,\n                },\n              },\n              y: [0, -FLOAT_AMPLITUDE, 0],\n            },\n      hover: (i: number) =>\n        shouldReduceMotion\n          ? {\n              rotate: 0,\n              x: 0,\n              y: 0,\n              zIndex: BASE_Z_INDEX - i * Z_INDEX_STEP,\n            }\n          : {\n              rotate: 0,\n              x: i * HOVER_X_MULTIPLIER,\n              y: -i * HOVER_Y_MULTIPLIER,\n              zIndex: BASE_Z_INDEX - i * Z_INDEX_STEP,\n            },\n      initial: (i: number) =>\n        shouldReduceMotion\n          ? {\n              rotate: 0,\n              x: 0,\n              y: 0,\n              zIndex: BASE_Z_INDEX - i * Z_INDEX_STEP,\n            }\n          : {\n              rotate:\n                i % 2 === 0\n                  ? -ROTATION_MULTIPLIER * (i + 1)\n                  : ROTATION_MULTIPLIER * (i + 1),\n              x:\n                i % 2 === 0\n                  ? -TRANSLATION_MULTIPLIER * (i + 1)\n                  : TRANSLATION_MULTIPLIER * (i + 1),\n              y: 0,\n              zIndex: BASE_Z_INDEX - i * Z_INDEX_STEP,\n            },\n    }),\n    [shouldReduceMotion]\n  );\n\n  return (\n    <div\n      className={`flex h-auto flex-col items-center justify-center ${className}`}\n    >\n      <motion.div\n        className=\"flex flex-col items-center justify-center\"\n        layout={!shouldReduceMotion}\n      >\n        <AnimatePresence mode=\"wait\">\n          {!(isExpanded || isDownloading) && (\n            <motion.button\n              aria-label=\"Expand app selection\"\n              className=\"group relative isolate flex h-16 w-16 cursor-pointer items-center justify-center\"\n              key=\"initial-stack\"\n              layout={!shouldReduceMotion}\n              onClick={() => setExpanded(true)}\n              whileHover={shouldReduceMotion ? {} : \"hover\"}\n            >\n              {apps.map((app, index) => (\n                <motion.img\n                  alt={`${app.name} Logo`}\n                  animate={\n                    shouldReduceMotion ? \"initial\" : [\"initial\", \"float\"]\n                  }\n                  className=\"absolute inset-0 rounded-xl border-none\"\n                  custom={index}\n                  draggable={false}\n                  height={64}\n                  initial=\"initial\"\n                  key={app.id}\n                  layoutId={\n                    shouldReduceMotion ? undefined : `app-icon-${app.id}`\n                  }\n                  src={app.icon}\n                  transition={\n                    shouldReduceMotion\n                      ? { duration: 0 }\n                      : { duration: TRANSITION_DURATION }\n                  }\n                  variants={stackVariants}\n                  whileHover={shouldReduceMotion ? {} : \"hover\"}\n                  width={64}\n                />\n              ))}\n            </motion.button>\n          )}\n\n          {isExpanded && !isDownloading && (\n            <motion.div\n              animate={\n                shouldReduceMotion ? { opacity: 1 } : { opacity: 1, scale: 1 }\n              }\n              className=\"flex flex-col items-center gap-2\"\n              exit={\n                shouldReduceMotion\n                  ? { opacity: 0, transition: { duration: 0 } }\n                  : { opacity: 0, scale: 0.8 }\n              }\n              initial={\n                shouldReduceMotion ? { opacity: 1 } : { opacity: 0, scale: 0.8 }\n              }\n              key=\"app-selector\"\n              layout={!shouldReduceMotion}\n            >\n              <button\n                className=\"flex w-full cursor-pointer items-center justify-between px-0.5\"\n                onClick={() => setExpanded(false)}\n                type=\"button\"\n              >\n                <p className=\"my-0 font-medium leading-0\">{title}</p>\n                <div className=\"flex items-center gap-1\">\n                  <p className=\"my-0 font-medium leading-0\">\n                    {selected.length}\n                  </p>\n                  <ChevronDown className=\"text-mauve-11\" size={16} />\n                </div>\n              </button>\n              <motion.ul className=\"grid grid-cols-2 gap-3\">\n                {apps.map((app, index) => (\n                  <motion.li\n                    animate={\n                      shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }\n                    }\n                    className=\"relative flex h-[80px] w-[80px]\"\n                    initial={\n                      shouldReduceMotion\n                        ? { opacity: 1 }\n                        : { opacity: 0, y: 20 }\n                    }\n                    key={app.id}\n                    transition={\n                      shouldReduceMotion\n                        ? { duration: 0 }\n                        : {\n                            delay: index * STAGGER_DELAY_MULTIPLIER,\n                            duration: 0.25,\n                          }\n                    }\n                  >\n                    <div\n                      className={`pointer-events-none absolute top-2 right-2 flex h-4 w-4 items-center justify-center rounded-full border border-solid ${\n                        selected.includes(app.id)\n                          ? \"border-blue-500 bg-blue-500\"\n                          : \"border-white/60\"\n                      }`}\n                    >\n                      {selected.includes(app.id) && (\n                        <motion.svg\n                          animate={{ pathLength: 1 }}\n                          className=\"z-1 h-3 w-3\"\n                          fill=\"none\"\n                          initial={{ pathLength: 0 }}\n                          stroke=\"white\"\n                          strokeLinecap=\"round\"\n                          strokeLinejoin=\"round\"\n                          strokeWidth=\"2\"\n                          transition={{\n                            duration: CHECKMARK_TRANSITION_DURATION,\n                          }}\n                          viewBox=\"0 0 24 24\"\n                          xmlns=\"http://www.w3.org/2000/svg\"\n                        >\n                          <title>Checkmark</title>\n                          <motion.path d=\"M5 13l4 4L19 7\" />\n                        </motion.svg>\n                      )}\n                    </div>\n                    <button\n                      className={`group flex h-full w-full flex-col items-center justify-center gap-1 rounded-xl border-2 border-transparent bg-background/80 p-2 transition-all duration-200 hover:border-blue-500 ${\n                        selected.includes(app.id)\n                          ? \"border-blue-500 bg-blue-500/10\"\n                          : \"\"\n                      }`}\n                      onClick={() => toggleApp(app.id)}\n                      type=\"button\"\n                    >\n                      <img\n                        alt={app.name}\n                        className=\"rounded-lg\"\n                        draggable={false}\n                        height={40}\n                        src={app.icon}\n                        width={40}\n                      />\n                      <span className=\"font-medium text-foreground text-xs\">\n                        {app.name}\n                      </span>\n                    </button>\n                  </motion.li>\n                ))}\n              </motion.ul>\n              <button\n                className=\"mt-4 w-full rounded-lg bg-blue-500 px-4 py-2 font-semibold text-white shadow transition hover:bg-blue-600 disabled:opacity-50\"\n                disabled={selected.length === 0}\n                onClick={handleDownload}\n                type=\"button\"\n              >\n                Download Selected\n              </button>\n            </motion.div>\n          )}\n\n          {isDownloading && !downloadComplete && (\n            <motion.div\n              animate={\n                shouldReduceMotion ? { opacity: 1 } : { opacity: 1, scale: 1 }\n              }\n              className=\"flex flex-col items-center gap-4\"\n              exit={\n                shouldReduceMotion\n                  ? { opacity: 0, transition: { duration: 0 } }\n                  : { opacity: 0, scale: 0.8 }\n              }\n              initial={\n                shouldReduceMotion ? { opacity: 1 } : { opacity: 0, scale: 0.8 }\n              }\n              key=\"downloading\"\n              layout={!shouldReduceMotion}\n            >\n              <div className=\"relative flex h-16 w-16 items-center justify-center\">\n                <motion.div\n                  animate={shineControls}\n                  className=\"absolute inset-0 rounded-xl bg-blue-500/20\"\n                  style={{ x: 0 }}\n                />\n                {apps.map((app, index) => (\n                  <motion.img\n                    alt={`${app.name} Logo`}\n                    animate={\n                      shouldReduceMotion ? \"initial\" : [\"initial\", \"float\"]\n                    }\n                    className=\"absolute inset-0 rounded-xl border-none\"\n                    custom={index}\n                    draggable={false}\n                    height={64}\n                    initial=\"initial\"\n                    key={app.id}\n                    layoutId={\n                      shouldReduceMotion ? undefined : `app-icon-${app.id}`\n                    }\n                    src={app.icon}\n                    transition={\n                      shouldReduceMotion\n                        ? { duration: 0 }\n                        : { duration: TRANSITION_DURATION }\n                    }\n                    variants={stackVariants}\n                    width={64}\n                  />\n                ))}\n              </div>\n              <span className=\"font-semibold text-blue-500\">\n                Downloading...\n              </span>\n            </motion.div>\n          )}\n\n          {downloadComplete ? (\n            <motion.div\n              animate={\n                shouldReduceMotion ? { opacity: 1 } : { opacity: 1, scale: 1 }\n              }\n              className=\"flex flex-col items-center gap-4\"\n              exit={\n                shouldReduceMotion\n                  ? { opacity: 0, transition: { duration: 0 } }\n                  : { opacity: 0, scale: 0.8 }\n              }\n              initial={\n                shouldReduceMotion ? { opacity: 1 } : { opacity: 0, scale: 0.8 }\n              }\n              key=\"download-complete\"\n              layout={!shouldReduceMotion}\n            >\n              <span className=\"font-semibold text-green-500\">\n                Download Complete!\n              </span>\n            </motion.div>\n          ) : null}\n        </AnimatePresence>\n      </motion.div>\n    </div>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/app-download-stack/index.tsx","type":"registry:ui"}],"name":"app-download-stack","registryDependencies":[],"title":"App Download Stack","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion","lucide-react","popmotion"],"description":"A AppleInvites component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { Crown } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { wrap } from \"popmotion\";\nimport { useEffect, useMemo, useRef, useState } from \"react\";\n\nexport interface ResponsiveSize {\n  \"2xl\"?: number | string;\n  base?: number | string;\n  lg?: number | string;\n  md?: number | string;\n  sm?: number | string;\n  xl?: number | string;\n}\n\nconst breakpoints = {\n  \"2xl\": 1536,\n  lg: 1024,\n  md: 768,\n  sm: 640,\n  xl: 1280,\n} as const;\n\nconst DEFAULT_CARD_WIDTH = 240;\nconst DEFAULT_ASPECT_RATIO = 1.5625; // 5:8 ratio (500/320)\n\n// Base sizes for responsive scaling (based on DEFAULT_CARD_WIDTH = 240)\nconst BASE_BADGE_FONT_SIZE = 12;\nconst BASE_BADGE_PADDING_X = 12;\nconst BASE_BADGE_PADDING_Y = 3;\nconst BASE_BADGE_ICON_SIZE = 14;\nconst BASE_TITLE_FONT_SIZE = 18;\nconst BASE_SUBTITLE_FONT_SIZE = 12;\nconst BASE_LOCATION_FONT_SIZE = 12;\nconst BASE_AVATAR_SIZE = 24;\nconst BASE_CONTENT_PADDING = 24;\nconst BASE_BADGE_TOP = 16;\nconst BASE_BADGE_LEFT = 16;\nconst BASE_BADGE_GAP = 8;\nconst BASE_AVATAR_GAP = 8;\nconst BASE_AVATAR_MARGIN_BOTTOM = 8;\nconst BASE_TITLE_MARGIN_BOTTOM = 4;\nconst BASE_LINE_HEIGHT = 1.4;\nconst BADGE_PADDING_Y_SCALE_FACTOR = 0.7; // Reduce vertical padding scaling for more compact badges\n\n// Minimum sizes to ensure readability\nconst MIN_BADGE_FONT_SIZE = 10;\nconst MIN_BADGE_PADDING_X = 8;\nconst MIN_BADGE_PADDING_Y = 1;\nconst MIN_BADGE_ICON_SIZE = 12;\nconst MIN_TITLE_FONT_SIZE = 14;\nconst MIN_SUBTITLE_FONT_SIZE = 10;\nconst MIN_LOCATION_FONT_SIZE = 10;\nconst MIN_AVATAR_SIZE = 20;\nconst MIN_CONTENT_PADDING = 12;\nconst MIN_BADGE_TOP = 8;\nconst MIN_BADGE_LEFT = 8;\nconst MIN_BADGE_GAP = 4;\nconst MIN_AVATAR_GAP = 4;\nconst MIN_AVATAR_MARGIN_BOTTOM = 4;\nconst MIN_TITLE_MARGIN_BOTTOM = 2;\n\nfunction formatSize(size: number | string): string {\n  return typeof size === \"number\" ? `${size}px` : size;\n}\n\nfunction getInitialSize(\n  size: number | string | ResponsiveSize | undefined,\n  defaultValue: number | string\n): string {\n  if (!size) {\n    return formatSize(defaultValue);\n  }\n  if (typeof size === \"number\" || typeof size === \"string\") {\n    return formatSize(size);\n  }\n  // Responsive object - start with base or first available value\n  if (size.base !== undefined) {\n    return formatSize(size.base);\n  }\n  return formatSize(defaultValue);\n}\n\nfunction getSizeForBreakpoint(\n  size: ResponsiveSize,\n  width: number\n): number | string | undefined {\n  if (width >= breakpoints[\"2xl\"]) {\n    return size[\"2xl\"] ?? size.xl ?? size.lg ?? size.md ?? size.sm ?? size.base;\n  }\n  if (width >= breakpoints.xl) {\n    return size.xl ?? size.lg ?? size.md ?? size.sm ?? size.base;\n  }\n  if (width >= breakpoints.lg) {\n    return size.lg ?? size.md ?? size.sm ?? size.base;\n  }\n  if (width >= breakpoints.md) {\n    return size.md ?? size.sm ?? size.base;\n  }\n  if (width >= breakpoints.sm) {\n    return size.sm ?? size.base;\n  }\n  return size.base;\n}\n\nfunction useResponsiveSize(\n  size: number | string | ResponsiveSize | undefined,\n  defaultValue: number | string\n): string {\n  const [currentSize, setCurrentSize] = useState<string>(() =>\n    getInitialSize(size, defaultValue)\n  );\n\n  useEffect(() => {\n    if (!size || typeof size === \"number\" || typeof size === \"string\") {\n      return;\n    }\n\n    const updateSize = () => {\n      const width = window.innerWidth;\n      const selectedSize = getSizeForBreakpoint(size, width);\n\n      if (selectedSize !== undefined) {\n        const newSize = formatSize(selectedSize);\n        setCurrentSize(newSize);\n      }\n    };\n\n    updateSize();\n    window.addEventListener(\"resize\", updateSize);\n    return () => window.removeEventListener(\"resize\", updateSize);\n  }, [size]);\n\n  return currentSize;\n}\n\nfunction parseSize(size: string): number {\n  const num = Number.parseFloat(size);\n  return Number.isNaN(num) ? 0 : num;\n}\n\nfunction calculateHeightFromWidth(width: string, aspectRatio: number): string {\n  const widthNum = parseSize(width);\n  if (widthNum === 0) {\n    return width;\n  }\n  const heightNum = widthNum * aspectRatio;\n  return `${heightNum}px`;\n}\n\nexport interface Participant {\n  avatar: string;\n}\n\nexport interface Event {\n  backgroundClassName?: string;\n  badge?: string;\n  id: number;\n  image?: string;\n  location: string;\n  participants?: Participant[];\n  subtitle?: string;\n  title?: string;\n}\n\nexport interface AppleInvitesProps {\n  activeIndex?: number;\n  aspectRatio?: number;\n  cardClassName?: string;\n  cardHeight?: number | string | ResponsiveSize;\n  cardWidth?: number | string | ResponsiveSize;\n  className?: string;\n  events: Event[];\n  interval?: number;\n  onChange?: (index: number) => void;\n}\n\nexport default function AppleInvites({\n  events,\n  interval = 3000,\n  className = \"\",\n  cardClassName = \"\",\n  activeIndex: controlledIndex,\n  onChange,\n  cardWidth = DEFAULT_CARD_WIDTH,\n  cardHeight,\n  aspectRatio = DEFAULT_ASPECT_RATIO,\n}: AppleInvitesProps) {\n  const shouldReduceMotion = useReducedMotion();\n  const [internalPage, setInternalPage] = useState(0);\n  const [direction, setDirection] = useState(0);\n  const responsiveWidth = useResponsiveSize(cardWidth, DEFAULT_CARD_WIDTH);\n\n  const variants = useMemo(\n    () => ({\n      center: {\n        opacity: 1,\n        rotate: 0,\n        scale: 1,\n        transition: shouldReduceMotion\n          ? { duration: 0 }\n          : {\n              damping: 30,\n              duration: 0.25,\n              stiffness: 300,\n              type: \"spring\" as const,\n            },\n        x: \"-50%\",\n        zIndex: 3,\n      },\n      hidden: {\n        opacity: 0,\n        transition: shouldReduceMotion ? { duration: 0 } : { duration: 0.3 },\n        zIndex: 1,\n      },\n      left: {\n        opacity: 0.8,\n        rotate: -12,\n        scale: 0.9,\n        transition: shouldReduceMotion\n          ? { duration: 0 }\n          : {\n              damping: 30,\n              duration: 0.25,\n              stiffness: 300,\n              type: \"spring\" as const,\n            },\n        x: \"-130%\",\n        zIndex: 2,\n      },\n      right: {\n        opacity: 0.8,\n        rotate: 12,\n        scale: 0.9,\n        transition: shouldReduceMotion\n          ? { duration: 0 }\n          : {\n              damping: 30,\n              duration: 0.25,\n              stiffness: 300,\n              type: \"spring\" as const,\n            },\n        x: \"30%\",\n        zIndex: 2,\n      },\n    }),\n    [shouldReduceMotion]\n  );\n  const explicitHeight = useResponsiveSize(\n    cardHeight,\n    calculateHeightFromWidth(responsiveWidth, aspectRatio)\n  );\n  const [calculatedHeight, setCalculatedHeight] = useState<string>(() =>\n    calculateHeightFromWidth(responsiveWidth, aspectRatio)\n  );\n\n  // Update calculated height when width changes (if using aspect ratio)\n  useEffect(() => {\n    if (cardHeight === undefined) {\n      setCalculatedHeight(\n        calculateHeightFromWidth(responsiveWidth, aspectRatio)\n      );\n    }\n  }, [responsiveWidth, aspectRatio, cardHeight]);\n\n  const responsiveHeight =\n    cardHeight === undefined ? calculatedHeight : explicitHeight;\n\n  // Calculate responsive sizes based on card width\n  const cardWidthNum = parseSize(responsiveWidth);\n  const scaleFactor = cardWidthNum / DEFAULT_CARD_WIDTH;\n\n  // Responsive sizes for internal content\n  const badgeFontSize = Math.max(\n    MIN_BADGE_FONT_SIZE,\n    Math.round(BASE_BADGE_FONT_SIZE * scaleFactor)\n  );\n  const badgePaddingX = Math.max(\n    MIN_BADGE_PADDING_X,\n    Math.round(BASE_BADGE_PADDING_X * scaleFactor)\n  );\n  // Use a more aggressive scaling for vertical padding to keep it compact\n  // Scale padding Y less aggressively to keep badges more compact\n  const badgePaddingY = Math.max(\n    MIN_BADGE_PADDING_Y,\n    Math.round(\n      BASE_BADGE_PADDING_Y * scaleFactor * BADGE_PADDING_Y_SCALE_FACTOR\n    )\n  );\n  const badgeIconSize = Math.max(\n    MIN_BADGE_ICON_SIZE,\n    Math.round(BASE_BADGE_ICON_SIZE * scaleFactor)\n  );\n  const titleFontSize = Math.max(\n    MIN_TITLE_FONT_SIZE,\n    Math.round(BASE_TITLE_FONT_SIZE * scaleFactor)\n  );\n  const subtitleFontSize = Math.max(\n    MIN_SUBTITLE_FONT_SIZE,\n    Math.round(BASE_SUBTITLE_FONT_SIZE * scaleFactor)\n  );\n  const locationFontSize = Math.max(\n    MIN_LOCATION_FONT_SIZE,\n    Math.round(BASE_LOCATION_FONT_SIZE * scaleFactor)\n  );\n  const avatarSize = Math.max(\n    MIN_AVATAR_SIZE,\n    Math.round(BASE_AVATAR_SIZE * scaleFactor)\n  );\n  const contentPadding = Math.max(\n    MIN_CONTENT_PADDING,\n    Math.round(BASE_CONTENT_PADDING * scaleFactor)\n  );\n  const badgeTop = Math.max(\n    MIN_BADGE_TOP,\n    Math.round(BASE_BADGE_TOP * scaleFactor)\n  );\n  const badgeLeft = Math.max(\n    MIN_BADGE_LEFT,\n    Math.round(BASE_BADGE_LEFT * scaleFactor)\n  );\n  const badgeGap = Math.max(\n    MIN_BADGE_GAP,\n    Math.round(BASE_BADGE_GAP * scaleFactor)\n  );\n  const avatarGap = Math.max(\n    MIN_AVATAR_GAP,\n    Math.round(BASE_AVATAR_GAP * scaleFactor)\n  );\n  const avatarMarginBottom = Math.max(\n    MIN_AVATAR_MARGIN_BOTTOM,\n    Math.round(BASE_AVATAR_MARGIN_BOTTOM * scaleFactor)\n  );\n  const titleMarginBottom = Math.max(\n    MIN_TITLE_MARGIN_BOTTOM,\n    Math.round(BASE_TITLE_MARGIN_BOTTOM * scaleFactor)\n  );\n\n  const page = controlledIndex === undefined ? internalPage : controlledIndex;\n  const setPage = (val: number, dir: number) => {\n    if (onChange) {\n      onChange(val);\n    } else {\n      setInternalPage(val);\n      setDirection(dir);\n    }\n  };\n\n  const activeIndex = wrap(0, events.length, page);\n  const setPageRef = useRef(setPage);\n\n  useEffect(() => {\n    setPageRef.current = setPage;\n  });\n\n  useEffect(() => {\n    const timer = setInterval(() => {\n      setPageRef.current(page + 1, 1);\n    }, interval);\n    return () => clearInterval(timer);\n  }, [page, interval]);\n\n  const visibleEvents = [-1, 0, 1].map(\n    (offset) => events[wrap(0, events.length, activeIndex + offset)]\n  );\n\n  const getVariant = (index: number) => {\n    if (index === 1) {\n      return \"center\";\n    }\n    if (index === 0) {\n      return \"left\";\n    }\n    return \"right\";\n  };\n\n  const renderBackground = (event: Event) => {\n    if (event.backgroundClassName) {\n      return <div className={`h-full w-full ${event.backgroundClassName}`} />;\n    }\n    if (event.image) {\n      return (\n        <img\n          alt={event.title || \"\"}\n          className=\"h-full w-full object-cover\"\n          draggable={false}\n          height={400}\n          src={event.image}\n          width={400}\n        />\n      );\n    }\n    return null;\n  };\n\n  return (\n    <div\n      className={`relative flex h-full w-full items-center justify-center ${className}`}\n    >\n      <AnimatePresence custom={direction} initial={false}>\n        {visibleEvents.map((event, index) => (\n          <motion.div\n            animate={getVariant(index)}\n            className={`absolute top-1/2 left-1/2 origin-center -translate-y-1/2 ${cardClassName}`}\n            custom={direction}\n            exit=\"hidden\"\n            initial=\"hidden\"\n            key={event.id}\n            style={{\n              height: responsiveHeight,\n              width: responsiveWidth,\n            }}\n            variants={variants}\n          >\n            <div className=\"relative h-full w-full overflow-hidden rounded-3xl bg-primary\">\n              {renderBackground(event)}\n              {/* Badge */}\n              <div\n                className=\"absolute z-3\"\n                style={{\n                  left: `${badgeLeft}px`,\n                  top: `${badgeTop}px`,\n                }}\n              >\n                <span\n                  className=\"flex flex-row items-center rounded-full bg-black/30 font-medium text-white backdrop-blur-xl\"\n                  style={{\n                    fontSize: `${badgeFontSize}px`,\n                    gap: `${badgeGap}px`,\n                    paddingBottom: `${badgePaddingY}px`,\n                    paddingLeft: `${badgePaddingX}px`,\n                    paddingRight: `${badgePaddingX}px`,\n                    paddingTop: `${badgePaddingY}px`,\n                  }}\n                >\n                  <Crown size={badgeIconSize} />\n                  {event.badge}\n                </span>\n              </div>\n              {/* Content */}\n              <div\n                className=\"absolute bottom-0 z-3 w-full rounded-b-3xl text-white\"\n                style={{ padding: `${contentPadding}px` }}\n              >\n                {/* Participant Avatars */}\n                <div\n                  className=\"mx-auto flex items-center justify-center\"\n                  style={{\n                    gap: `${avatarGap}px`,\n                    marginBottom: `${avatarMarginBottom}px`,\n                  }}\n                >\n                  {event.participants?.map((participant, idx) => (\n                    <img\n                      alt={`Participant ${idx + 1}`}\n                      className=\"rounded-full\"\n                      draggable={false}\n                      height={avatarSize}\n                      key={`participant-${participant.avatar}-${idx}`}\n                      src={participant.avatar}\n                      style={{\n                        height: `${avatarSize}px`,\n                        width: `${avatarSize}px`,\n                      }}\n                      width={avatarSize}\n                    />\n                  ))}\n                </div>\n                {event.title ? (\n                  <p\n                    className=\"wrap-break-word text-center font-bold\"\n                    style={{\n                      fontSize: `${titleFontSize}px`,\n                      lineHeight: BASE_LINE_HEIGHT,\n                      marginBottom: `${titleMarginBottom}px`,\n                    }}\n                  >\n                    {event.title}\n                  </p>\n                ) : null}\n                {event.subtitle ? (\n                  <p\n                    className=\"wrap-break-word text-center opacity-90\"\n                    style={{\n                      fontSize: `${subtitleFontSize}px`,\n                      lineHeight: BASE_LINE_HEIGHT,\n                    }}\n                  >\n                    {event.subtitle}\n                  </p>\n                ) : null}\n                <p\n                  className=\"wrap-break-word text-center opacity-90\"\n                  style={{\n                    fontSize: `${locationFontSize}px`,\n                    lineHeight: BASE_LINE_HEIGHT,\n                  }}\n                >\n                  {event.location}\n                </p>\n              </div>\n              <div className=\"fixed inset-x-0 bottom-0 isolate z-2 h-1/2\">\n                <div className=\"gradient-mask-t-0 absolute inset-0 overflow-hidden rounded-3xl backdrop-blur-[1px]\" />\n                <div className=\"gradient-mask-t-0 absolute inset-0 overflow-hidden rounded-3xl backdrop-blur-[2px]\" />\n                <div className=\"gradient-mask-t-0 absolute inset-0 overflow-hidden rounded-3xl backdrop-blur-[3px]\" />\n                <div className=\"gradient-mask-t-0 absolute inset-0 overflow-hidden rounded-3xl backdrop-blur-[6px]\" />\n                <div className=\"gradient-mask-t-0 absolute inset-0 overflow-hidden rounded-3xl backdrop-blur-[12px]\" />\n              </div>\n            </div>\n          </motion.div>\n        ))}\n      </AnimatePresence>\n    </div>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/apple-invites/index.tsx","type":"registry:ui"}],"name":"apple-invites","registryDependencies":[],"title":"Apple Invites","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion","lucide-react"],"description":"A BasicAccordion component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { ChevronDown } from \"lucide-react\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { useState } from \"react\";\n\nconst CHEVRON_ROTATION_DEGREES = 180;\nconst CHEVRON_ANIMATION_DURATION = 0.2;\n\nexport interface AccordionItem {\n  content: React.ReactNode;\n  id: string | number;\n  title: string;\n}\n\nexport interface BasicAccordionProps {\n  allowMultiple?: boolean;\n  className?: string;\n  defaultExpandedIds?: Array<string | number>;\n  items: AccordionItem[];\n}\n\nexport default function BasicAccordion({\n  items,\n  allowMultiple = false,\n  className = \"\",\n  defaultExpandedIds = [],\n}: BasicAccordionProps) {\n  const [expandedItems, setExpandedItems] =\n    useState<Array<string | number>>(defaultExpandedIds);\n  const shouldReduceMotion = useReducedMotion();\n\n  const toggleItem = (id: string | number) => {\n    if (expandedItems.includes(id)) {\n      setExpandedItems(expandedItems.filter((item) => item !== id));\n    } else if (allowMultiple) {\n      setExpandedItems([...expandedItems, id]);\n    } else {\n      setExpandedItems([id]);\n    }\n  };\n\n  return (\n    <div\n      className={`flex w-full flex-col divide-y divide-border overflow-hidden rounded-lg border ${className}`}\n    >\n      {items.map((item) => {\n        const isExpanded = expandedItems.includes(item.id);\n\n        return (\n          <div className=\"overflow-hidden\" key={item.id}>\n            <button\n              aria-controls={`accordion-content-${item.id}`}\n              aria-expanded={isExpanded}\n              className=\"flex min-h-[44px] w-full cursor-pointer items-center justify-between gap-2 bg-background px-4 py-3 text-left transition-colors hover:bg-primary focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\"\n              id={`accordion-header-${item.id}`}\n              onClick={() => toggleItem(item.id)}\n              type=\"button\"\n            >\n              <h3 className=\"font-medium\">{item.title}</h3>\n              <motion.div\n                animate={{ rotate: isExpanded ? CHEVRON_ROTATION_DEGREES : 0 }}\n                className=\"shrink-0\"\n                transition={{\n                  duration: shouldReduceMotion ? 0 : CHEVRON_ANIMATION_DURATION,\n                }}\n              >\n                <ChevronDown className=\"h-5 w-5\" />\n              </motion.div>\n            </button>\n\n            {/* Content stays mounted (height-animated, not unmounted) so the\n                accordion keeps a stable width whether open or closed. `inert`\n                removes collapsed content from tab order and the a11y tree. */}\n            <motion.div\n              animate={{\n                height: isExpanded ? \"auto\" : 0,\n                opacity: isExpanded ? 1 : 0,\n              }}\n              aria-labelledby={`accordion-header-${item.id}`}\n              className=\"overflow-hidden\"\n              id={`accordion-content-${item.id}`}\n              inert={!isExpanded}\n              initial={false}\n              role=\"region\"\n              transition={\n                shouldReduceMotion\n                  ? { duration: 0 }\n                  : {\n                      height: {\n                        damping: 40,\n                        duration: 0.25,\n                        stiffness: 500,\n                        type: \"spring\" as const,\n                      },\n                      opacity: { duration: 0.2 },\n                    }\n              }\n            >\n              <div className=\"border-t bg-background px-4 py-3\">\n                {item.content}\n              </div>\n            </motion.div>\n          </div>\n        );\n      })}\n    </div>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/basic-accordion/index.tsx","type":"registry:ui"}],"name":"basic-accordion","registryDependencies":[],"title":"Basic Accordion","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion","lucide-react"],"description":"A BasicDropdown component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { ChevronDown } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { createPortal } from \"react-dom\";\n\nconst ROTATION_ANGLE_OPEN = 180;\nconst DROPDOWN_OFFSET = 4;\n\nexport interface DropdownItem {\n  icon?: React.ReactNode;\n  id: string | number;\n  label: string;\n}\n\nexport interface BasicDropdownProps {\n  className?: string;\n  items: DropdownItem[];\n  label: string;\n  onChange?: (item: DropdownItem) => void;\n}\n\nexport default function BasicDropdown({\n  label,\n  items,\n  onChange,\n  className = \"\",\n}: BasicDropdownProps) {\n  const [isOpen, setIsOpen] = useState(false);\n  const [selectedItem, setSelectedItem] = useState<DropdownItem | null>(null);\n  const [focusedIndex, setFocusedIndex] = useState(-1);\n  const dropdownRef = useRef<HTMLDivElement>(null);\n  const buttonRef = useRef<HTMLButtonElement>(null);\n  const portalRef = useRef<HTMLDivElement>(null);\n  const [position, setPosition] = useState({ left: 0, top: 0, width: 0 });\n  const shouldReduceMotion = useReducedMotion();\n\n  const handleItemSelect = (item: DropdownItem) => {\n    setSelectedItem(item);\n    setIsOpen(false);\n    onChange?.(item);\n  };\n\n  const handleToggle = () => {\n    if (!isOpen && buttonRef.current) {\n      const rect = buttonRef.current.getBoundingClientRect();\n      setPosition({\n        left: rect.left,\n        top: rect.bottom + DROPDOWN_OFFSET,\n        width: rect.width,\n      });\n    }\n    setIsOpen(!isOpen);\n  };\n\n  // Update position on scroll/resize when open\n  useEffect(() => {\n    if (!(isOpen && buttonRef.current)) {\n      return;\n    }\n\n    const updatePosition = () => {\n      if (buttonRef.current) {\n        const rect = buttonRef.current.getBoundingClientRect();\n        setPosition({\n          left: rect.left,\n          top: rect.bottom + DROPDOWN_OFFSET,\n          width: rect.width,\n        });\n      }\n    };\n\n    window.addEventListener(\"scroll\", updatePosition, true);\n    window.addEventListener(\"resize\", updatePosition);\n\n    return () => {\n      window.removeEventListener(\"scroll\", updatePosition, true);\n      window.removeEventListener(\"resize\", updatePosition);\n    };\n  }, [isOpen]);\n\n  // Close dropdown when clicking outside\n  useEffect(() => {\n    const handleClickOutside = (event: MouseEvent) => {\n      const target = event.target as Node;\n      if (\n        isOpen &&\n        dropdownRef.current &&\n        !dropdownRef.current.contains(target) &&\n        portalRef.current &&\n        !portalRef.current.contains(target)\n      ) {\n        setIsOpen(false);\n        setFocusedIndex(-1);\n      }\n    };\n\n    if (isOpen) {\n      document.addEventListener(\"mousedown\", handleClickOutside);\n    }\n    return () => {\n      document.removeEventListener(\"mousedown\", handleClickOutside);\n    };\n  }, [isOpen]);\n\n  // Keyboard navigation\n  useEffect(() => {\n    const handleKeyDown = (event: KeyboardEvent) => {\n      if (!isOpen) {\n        // Open dropdown on Enter or Space when button is focused\n        if (\n          (event.key === \"Enter\" || event.key === \" \") &&\n          document.activeElement === buttonRef.current\n        ) {\n          event.preventDefault();\n          handleToggle();\n        }\n        return;\n      }\n\n      if (event.key === \"Escape\") {\n        setIsOpen(false);\n        setFocusedIndex(-1);\n        buttonRef.current?.focus();\n      } else if (event.key === \"ArrowDown\") {\n        event.preventDefault();\n        setFocusedIndex((prev) => (prev < items.length - 1 ? prev + 1 : 0));\n      } else if (event.key === \"ArrowUp\") {\n        event.preventDefault();\n        setFocusedIndex((prev) => (prev > 0 ? prev - 1 : items.length - 1));\n      } else if (event.key === \"Enter\" && focusedIndex >= 0) {\n        event.preventDefault();\n        const item = items[focusedIndex];\n        if (item) {\n          handleItemSelect(item);\n        }\n      } else if (event.key === \"Home\") {\n        event.preventDefault();\n        setFocusedIndex(0);\n      } else if (event.key === \"End\") {\n        event.preventDefault();\n        setFocusedIndex(items.length - 1);\n      }\n    };\n\n    document.addEventListener(\"keydown\", handleKeyDown);\n    return () => document.removeEventListener(\"keydown\", handleKeyDown);\n    // biome-ignore lint/correctness/useExhaustiveDependencies: Handlers are stable via closure\n  }, [isOpen, items, focusedIndex, handleItemSelect, handleToggle]);\n\n  // Reset focused index when items change\n  useEffect(() => {\n    setFocusedIndex(-1);\n  }, []);\n\n  const dropdownContent = (\n    <AnimatePresence>\n      {isOpen ? (\n        <div ref={portalRef}>\n          <motion.div\n            animate={\n              shouldReduceMotion\n                ? { opacity: 1 }\n                : { opacity: 1, scaleY: 1, y: 0 }\n            }\n            className=\"fixed z-50 origin-top rounded-lg border bg-background shadow-lg\"\n            exit={\n              shouldReduceMotion\n                ? { opacity: 0, transition: { duration: 0 } }\n                : {\n                    opacity: 0,\n                    scaleY: 0.8,\n                    transition: { duration: 0.15 },\n                    y: -10,\n                  }\n            }\n            initial={\n              shouldReduceMotion\n                ? { opacity: 1 }\n                : { opacity: 0, scaleY: 0.8, y: -10 }\n            }\n            style={{\n              left: `${position.left}px`,\n              top: `${position.top}px`,\n              width: `${position.width}px`,\n            }}\n            transition={\n              shouldReduceMotion\n                ? { duration: 0 }\n                : { bounce: 0.1, duration: 0.25, type: \"spring\" as const }\n            }\n          >\n            <ul\n              aria-label=\"Dropdown options\"\n              className=\"py-2\"\n              id=\"dropdown-items\"\n            >\n              {items.map((item, index) => (\n                <motion.li\n                  animate={\n                    shouldReduceMotion ? { opacity: 1 } : { opacity: 1, x: 0 }\n                  }\n                  aria-selected={\n                    selectedItem?.id === item.id || index === focusedIndex\n                  }\n                  className=\"block\"\n                  exit={\n                    shouldReduceMotion\n                      ? { opacity: 0, transition: { duration: 0 } }\n                      : { opacity: 0, x: -10 }\n                  }\n                  initial={\n                    shouldReduceMotion ? { opacity: 1 } : { opacity: 0, x: -10 }\n                  }\n                  key={item.id}\n                  role=\"option\"\n                  transition={\n                    shouldReduceMotion\n                      ? { duration: 0 }\n                      : {\n                          damping: 30,\n                          duration: 0.2,\n                          stiffness: 300,\n                          type: \"spring\" as const,\n                        }\n                  }\n                  whileHover={shouldReduceMotion ? {} : { x: 5 }}\n                >\n                  <button\n                    aria-label={item.label}\n                    className={`flex min-h-[44px] w-full items-center px-4 py-2 text-left text-sm transition-colors hover:bg-muted focus-visible:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ${\n                      selectedItem?.id === item.id\n                        ? \"font-medium text-brand\"\n                        : \"\"\n                    } ${index === focusedIndex ? \"bg-muted\" : \"\"}`}\n                    onClick={() => handleItemSelect(item)}\n                    onMouseEnter={() => setFocusedIndex(index)}\n                    type=\"button\"\n                  >\n                    {item.icon ? (\n                      <span className=\"mr-2\">{item.icon}</span>\n                    ) : null}\n                    {item.label}\n\n                    {selectedItem?.id === item.id && (\n                      <motion.span\n                        animate={shouldReduceMotion ? {} : { scale: 1 }}\n                        className=\"ml-auto\"\n                        initial={shouldReduceMotion ? {} : { scale: 0 }}\n                        transition={\n                          shouldReduceMotion\n                            ? { duration: 0 }\n                            : {\n                                damping: 20,\n                                duration: 0.2,\n                                stiffness: 300,\n                                type: \"spring\" as const,\n                              }\n                        }\n                      >\n                        <svg\n                          className=\"h-4 w-4 text-brand\"\n                          fill=\"none\"\n                          stroke=\"currentColor\"\n                          viewBox=\"0 0 24 24\"\n                        >\n                          <title>Selected</title>\n                          <path\n                            d=\"M5 13l4 4L19 7\"\n                            strokeLinecap=\"round\"\n                            strokeLinejoin=\"round\"\n                            strokeWidth={2}\n                          />\n                        </svg>\n                      </motion.span>\n                    )}\n                  </button>\n                </motion.li>\n              ))}\n            </ul>\n          </motion.div>\n        </div>\n      ) : null}\n    </AnimatePresence>\n  );\n\n  return (\n    <>\n      <div className={`relative inline-block ${className}`} ref={dropdownRef}>\n        <button\n          aria-expanded={isOpen}\n          aria-haspopup=\"listbox\"\n          aria-label={selectedItem ? `${label}: ${selectedItem.label}` : label}\n          className=\"flex min-h-[44px] w-full cursor-pointer items-center justify-between gap-2 rounded-lg border bg-background px-4 py-2 text-left transition-colors hover:bg-primary focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\"\n          id=\"dropdown-button\"\n          onClick={handleToggle}\n          ref={buttonRef}\n          type=\"button\"\n        >\n          <span className=\"block truncate\">\n            {String(selectedItem ? selectedItem.label : label)}\n          </span>\n          <motion.div\n            animate={{ rotate: isOpen ? ROTATION_ANGLE_OPEN : 0 }}\n            transition={{ duration: shouldReduceMotion ? 0 : 0.2 }}\n          >\n            <ChevronDown className=\"h-4 w-4\" />\n          </motion.div>\n        </button>\n      </div>\n      {typeof window === \"undefined\"\n        ? null\n        : createPortal(dropdownContent, document.body)}\n    </>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/basic-dropdown/index.tsx","type":"registry:ui"}],"name":"basic-dropdown","registryDependencies":["https://smoothui.dev/r/tokens.json"],"title":"Basic Dropdown","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion","lucide-react","usehooks-ts"],"description":"A BasicModal component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { X } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { useOnClickOutside } from \"usehooks-ts\";\n\nexport interface BasicModalProps {\n  children: React.ReactNode;\n  isOpen: boolean;\n  onClose: () => void;\n  size?: \"sm\" | \"md\" | \"lg\" | \"xl\" | \"full\";\n  title?: string;\n}\n\nconst modalSizes = {\n  full: \"max-w-4xl\",\n  lg: \"max-w-lg\",\n  md: \"max-w-md\",\n  sm: \"max-w-sm\",\n  xl: \"max-w-xl\",\n};\n\nexport default function BasicModal({\n  isOpen,\n  onClose,\n  title,\n  children,\n  size = \"md\",\n}: BasicModalProps) {\n  const overlayRef = useRef<HTMLDivElement>(null);\n  const modalRef = useRef<HTMLDivElement>(\n    null\n  ) as React.RefObject<HTMLDivElement>;\n  const closeButtonRef = useRef<HTMLButtonElement>(null);\n  const previousActiveElementRef = useRef<HTMLElement | null>(null);\n  useOnClickOutside(modalRef, () => onClose());\n  const [mounted, setMounted] = useState(false);\n  const shouldReduceMotion = useReducedMotion();\n\n  const titleId = title\n    ? `modal-title-${Math.random().toString(36).substring(2, 9)}`\n    : undefined;\n\n  useEffect(() => {\n    setMounted(true);\n  }, []);\n\n  // Focus management: Save previous focus and restore on close\n  useEffect(() => {\n    if (isOpen) {\n      previousActiveElementRef.current = document.activeElement as HTMLElement;\n      // Focus the close button or first focusable element when modal opens\n      setTimeout(() => {\n        closeButtonRef.current?.focus();\n      }, 100);\n    } else if (previousActiveElementRef.current) {\n      // Restore focus when modal closes\n      previousActiveElementRef.current.focus();\n    }\n  }, [isOpen]);\n\n  // Close on Escape key press and focus trap\n  useEffect(() => {\n    if (!isOpen) {\n      return;\n    }\n\n    const handleKeyDown = (e: KeyboardEvent) => {\n      if (e.key === \"Escape\") {\n        onClose();\n        return;\n      }\n\n      // Focus trap: keep focus within modal\n      if (e.key === \"Tab\" && modalRef.current) {\n        const focusableElements = Array.from(\n          modalRef.current.querySelectorAll<HTMLElement>(\n            'button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])'\n          )\n        );\n        const [firstElement] = focusableElements;\n        const lastElement = focusableElements.at(-1);\n\n        if (e.shiftKey) {\n          // Shift + Tab\n          if (document.activeElement === firstElement) {\n            e.preventDefault();\n            lastElement?.focus();\n          }\n        } else if (document.activeElement === lastElement) {\n          // Tab\n          e.preventDefault();\n          firstElement?.focus();\n        }\n      }\n    };\n\n    document.addEventListener(\"keydown\", handleKeyDown);\n    return () => document.removeEventListener(\"keydown\", handleKeyDown);\n  }, [isOpen, onClose]);\n\n  // Note: Body scroll locking is handled by the overlay and modal positioning\n  // No need to manually set body overflow as it can conflict with other components\n\n  const modalContent = (\n    <AnimatePresence>\n      {isOpen ? (\n        <>\n          {/* Backdrop */}\n          <motion.div\n            animate={{ opacity: 1 }}\n            className=\"fixed inset-0 z-[80] bg-background/70 backdrop-blur-sm\"\n            exit={{ opacity: 0 }}\n            initial={shouldReduceMotion ? { opacity: 1 } : { opacity: 0 }}\n            onClick={(e) => {\n              if (e.target === overlayRef.current) {\n                onClose();\n              }\n            }}\n            ref={overlayRef}\n            transition={{ duration: shouldReduceMotion ? 0 : 0.2 }}\n          />\n\n          {/* Modal */}\n          <motion.div\n            animate={{ opacity: 1 }}\n            className=\"fixed inset-0 z-[90] flex items-center justify-center overflow-y-auto px-4 py-6 sm:p-0\"\n            exit={{ opacity: 0 }}\n            initial={shouldReduceMotion ? { opacity: 1 } : { opacity: 0 }}\n            transition={{ duration: shouldReduceMotion ? 0 : 0.2 }}\n          >\n            <motion.div\n              animate={shouldReduceMotion ? {} : { opacity: 1, scale: 1, y: 0 }}\n              aria-labelledby={titleId}\n              aria-modal=\"true\"\n              className={`${modalSizes[size]} relative mx-auto w-full rounded-xl border bg-primary p-4 shadow-xl sm:p-6`}\n              exit={\n                shouldReduceMotion\n                  ? { opacity: 0, transition: { duration: 0 } }\n                  : {\n                      opacity: 0,\n                      scale: 0.95,\n                      transition: { duration: 0.15 },\n                      y: 10,\n                    }\n              }\n              initial={\n                shouldReduceMotion\n                  ? { opacity: 1 }\n                  : { opacity: 0, scale: 0.95, y: 10 }\n              }\n              ref={modalRef}\n              role=\"dialog\"\n              transition={\n                shouldReduceMotion\n                  ? { duration: 0 }\n                  : {\n                      damping: 25,\n                      duration: 0.25,\n                      stiffness: 300,\n                      type: \"spring\" as const,\n                    }\n              }\n            >\n              {/* Header */}\n              <div className=\"mb-4 flex items-center justify-between\">\n                {title ? (\n                  <h3 className=\"font-medium text-xl leading-6\" id={titleId}>\n                    {title}\n                  </h3>\n                ) : null}\n                <motion.button\n                  aria-label=\"Close modal\"\n                  className=\"ml-auto min-h-[44px] min-w-[44px] cursor-pointer rounded-full p-2 transition-colors hover:bg-secondary focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\"\n                  onClick={onClose}\n                  ref={closeButtonRef}\n                  transition={{ duration: shouldReduceMotion ? 0 : 0.2 }}\n                  type=\"button\"\n                  whileHover={shouldReduceMotion ? {} : { rotate: 90 }}\n                >\n                  <X aria-hidden=\"true\" className=\"h-5 w-5\" />\n                </motion.button>\n              </div>\n\n              {/* Content */}\n              <div className=\"relative\">{children}</div>\n            </motion.div>\n          </motion.div>\n        </>\n      ) : null}\n    </AnimatePresence>\n  );\n\n  if (!mounted) {\n    return null;\n  }\n\n  return createPortal(modalContent, document.body);\n}\n","path":"index.tsx","target":"components/smoothui/basic-modal/index.tsx","type":"registry:ui"}],"name":"basic-modal","registryDependencies":[],"title":"Basic Modal","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion","lucide-react"],"description":"A BasicToast component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { AlertCircle, CheckCircle, Info, X, XCircle } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useEffect, useState } from \"react\";\nimport { createPortal } from \"react-dom\";\n\nexport type ToastType = \"success\" | \"error\" | \"info\" | \"warning\";\n\nexport interface ToastProps {\n  className?: string;\n  duration?: number;\n  isVisible?: boolean;\n  message: string;\n  onClose?: () => void;\n  type?: ToastType;\n}\n\nconst toastIcons = {\n  error: <XCircle className=\"h-5 w-5 text-red-500\" />,\n  info: <Info className=\"h-5 w-5 text-blue-500\" />,\n  success: <CheckCircle className=\"h-5 w-5 text-emerald-500\" />,\n  warning: <AlertCircle className=\"h-5 w-5 text-amber-500\" />,\n};\n\nconst toastClasses = {\n  error: \"border-red-100 bg-red-50 dark:border-red-900 dark:bg-red-950\",\n  info: \"border-blue-100 bg-blue-50 dark:border-blue-900 dark:bg-blue-950\",\n  success:\n    \"border-emerald-100 bg-emerald-50 dark:border-emerald-900 dark:bg-emerald-950\",\n  warning:\n    \"border-amber-100 bg-amber-50 dark:border-amber-900 dark:bg-amber-950\",\n};\n\nexport default function BasicToast({\n  message,\n  type = \"info\",\n  duration = 3000,\n  onClose,\n  isVisible = true,\n  className = \"\",\n}: ToastProps) {\n  const [visible, setVisible] = useState(isVisible);\n  const [mounted, setMounted] = useState(false);\n  const shouldReduceMotion = useReducedMotion();\n\n  useEffect(() => {\n    setMounted(true);\n  }, []);\n\n  useEffect(() => {\n    setVisible(isVisible);\n  }, [isVisible]);\n\n  useEffect(() => {\n    if (visible && duration > 0) {\n      const timer = setTimeout(() => {\n        setVisible(false);\n        onClose?.();\n      }, duration);\n      return () => clearTimeout(timer);\n    }\n  }, [visible, duration, onClose]);\n\n  if (!mounted) {\n    return null;\n  }\n\n  const toastContent = (\n    <AnimatePresence>\n      {visible ? (\n        <motion.div\n          animate={\n            shouldReduceMotion ? { opacity: 1 } : { opacity: 1, scale: 1, x: 0 }\n          }\n          className={`fixed top-4 right-4 z-50 flex w-80 items-center gap-3 rounded-lg border p-4 shadow-lg ${toastClasses[type]} ${className}`}\n          exit={\n            shouldReduceMotion\n              ? { opacity: 0, transition: { duration: 0 } }\n              : {\n                  opacity: 0,\n                  scale: 0.8,\n                  transition: { duration: 0.15 },\n                  x: 50,\n                }\n          }\n          initial={\n            shouldReduceMotion\n              ? { opacity: 1 }\n              : { opacity: 0, scale: 0.8, x: 50 }\n          }\n          transition={\n            shouldReduceMotion\n              ? { duration: 0 }\n              : { bounce: 0.1, duration: 0.25, type: \"spring\" as const }\n          }\n        >\n          <div className=\"flex-shrink-0\">{toastIcons[type]}</div>\n          <p className=\"flex-1 text-sm\">{message}</p>\n          <button\n            className=\"flex-shrink-0 cursor-pointer rounded-full p-1 transition-colors hover:bg-black/5 dark:hover:bg-white/10\"\n            onClick={() => {\n              setVisible(false);\n              onClose?.();\n            }}\n            type=\"button\"\n          >\n            <X className=\"h-4 w-4\" />\n          </button>\n        </motion.div>\n      ) : null}\n    </AnimatePresence>\n  );\n\n  return createPortal(toastContent, document.body);\n}\n\n// Example of how to use this component:\nexport function ToastDemo() {\n  const [showToast, setShowToast] = useState(false);\n  const [toastType, setToastType] = useState<ToastType>(\"success\");\n\n  const handleShowToast = (type: ToastType) => {\n    setToastType(type);\n    setShowToast(true);\n  };\n\n  return (\n    <div className=\"flex flex-col gap-4 p-4\">\n      <div className=\"flex flex-wrap gap-2\">\n        <button\n          className=\"cursor-pointer rounded-md bg-emerald-500 px-3 py-1.5 text-sm text-white hover:bg-emerald-600\"\n          onClick={() => handleShowToast(\"success\")}\n          type=\"button\"\n        >\n          Success Toast\n        </button>\n        <button\n          className=\"cursor-pointer rounded-md bg-red-500 px-3 py-1.5 text-sm text-white hover:bg-red-600\"\n          onClick={() => handleShowToast(\"error\")}\n          type=\"button\"\n        >\n          Error Toast\n        </button>\n        <button\n          className=\"cursor-pointer rounded-md bg-amber-500 px-3 py-1.5 text-sm text-white hover:bg-amber-600\"\n          onClick={() => handleShowToast(\"warning\")}\n          type=\"button\"\n        >\n          Warning Toast\n        </button>\n        <button\n          className=\"cursor-pointer rounded-md bg-blue-500 px-3 py-1.5 text-sm text-white hover:bg-blue-600\"\n          onClick={() => handleShowToast(\"info\")}\n          type=\"button\"\n        >\n          Info Toast\n        </button>\n      </div>\n\n      <AnimatePresence>\n        {showToast ? (\n          <BasicToast\n            duration={3000}\n            message={`This is a ${toastType} message example!`}\n            onClose={() => setShowToast(false)}\n            type={toastType}\n          />\n        ) : null}\n      </AnimatePresence>\n    </div>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/basic-toast/index.tsx","type":"registry:ui"}],"name":"basic-toast","registryDependencies":[],"title":"Basic Toast","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"A BlurOutUp text animation component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { motion, useInView, useReducedMotion } from \"motion/react\";\nimport { useRef } from \"react\";\n\n/**\n * BlurOutUp — words arrive clean from a gentle blur and upward drift,\n * Apple-style airy entrance. From the animate-text catalog (`blur-out-up`).\n */\nexport interface BlurOutUpProps {\n  children: string;\n  className?: string;\n  /** Delay before the animation starts, in milliseconds. */\n  delay?: number;\n  /** Per-word stagger, in milliseconds. */\n  stagger?: number;\n  /** Animate only once the text scrolls into view. */\n  triggerOnView?: boolean;\n}\n\nconst DURATION_S = 0.56;\nconst MS = 1000;\nconst EASE = [0.22, 1, 0.36, 1] as const;\n\nexport default function BlurOutUp({\n  children,\n  className = \"\",\n  delay = 0,\n  stagger = 28,\n  triggerOnView = false,\n}: BlurOutUpProps) {\n  const ref = useRef<HTMLSpanElement>(null);\n  const inView = useInView(ref, { once: true });\n  const shouldReduceMotion = useReducedMotion();\n  const play = (!triggerOnView || inView) && !shouldReduceMotion;\n  const words = children.split(\" \");\n\n  return (\n    <span aria-label={children} className={className} ref={ref}>\n      {words.map((word, index) => (\n        // biome-ignore lint/suspicious/noArrayIndexKey: words have no stable id\n        <span key={index}>\n          <motion.span\n            animate={\n              play ? { filter: \"blur(0px)\", opacity: 1, y: 0 } : undefined\n            }\n            aria-hidden=\"true\"\n            initial={\n              shouldReduceMotion\n                ? { opacity: 1 }\n                : { filter: \"blur(6px)\", opacity: 0, y: 10 }\n            }\n            style={{ display: \"inline-block\", whiteSpace: \"pre\" }}\n            transition={\n              shouldReduceMotion\n                ? { duration: 0 }\n                : {\n                    delay: delay / MS + (index * stagger) / MS,\n                    duration: DURATION_S,\n                    ease: EASE,\n                  }\n            }\n          >\n            {word}\n          </motion.span>\n          {index < words.length - 1 && (\n            <span\n              aria-hidden=\"true\"\n              style={{ display: \"inline-block\", whiteSpace: \"pre\" }}\n            >\n              {\" \"}\n            </span>\n          )}\n        </span>\n      ))}\n    </span>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/blur-out-up/index.tsx","type":"registry:ui"}],"name":"blur-out-up","registryDependencies":[],"title":"Blur Out Up","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"A 3D CSS book component with perspective transforms and hover animation","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { useReducedMotion } from \"motion/react\";\nimport type { ReactNode } from \"react\";\n\nexport type BookVariant = \"stripe\" | \"simple\";\n\nexport interface BookProps {\n  /** Additional CSS classes */\n  className?: string;\n  /** Cover accent color (CSS color value) */\n  color?: string;\n  /** Custom illustration for the stripe area (stripe variant only) */\n  illustration?: ReactNode;\n  /** Logo or icon displayed at the bottom of the content area */\n  logo?: ReactNode;\n  /** Title text color override */\n  textColor?: string;\n  /** Book title text displayed on the cover */\n  title: string;\n  /** Visual variant of the book */\n  variant?: BookVariant;\n  /** Book width in pixels */\n  width?: number;\n}\n\nconst BOOK_DEPTH = \"29cqw\";\nconst BOOK_BORDER_RADIUS = \"6px 4px 4px 6px\";\nconst DEFAULT_WIDTH = 196;\nconst DEFAULT_COLOR = \"#e5a00d\";\n\nconst BG_SHADOW =\n  \"linear-gradient(90deg, #fff0 0%, #fff0 12%, #ffffff40 29.25%, #fff0 50.5%, #fff0 75.25%, #ffffff40 91%, #fff0 100%), linear-gradient(90deg, #00000008 0%, #0000001a 12%, #0000 30%, #00000005 50%, #0003 73.5%, #00000080 75.25%, #00000026 85.25%, #0000 100%)\";\n\nconst Book = ({\n  title,\n  variant = \"stripe\",\n  color = DEFAULT_COLOR,\n  textColor,\n  width = DEFAULT_WIDTH,\n  illustration,\n  logo,\n  className,\n}: BookProps) => {\n  const shouldReduceMotion = useReducedMotion();\n  const bookWidth = `${width}px`;\n\n  return (\n    <div\n      className={cn(\"inline-block w-fit\", className)}\n      style={{\n        perspective: \"900px\",\n        // CSS custom properties for internal use\n        [\"--book-width\" as string]: bookWidth,\n        [\"--book-color\" as string]: color,\n        [\"--book-text-color\" as string]: textColor || \"var(--color-foreground)\",\n        [\"--book-depth\" as string]: BOOK_DEPTH,\n        [\"--book-border-radius\" as string]: BOOK_BORDER_RADIUS,\n        [\"--bg-shadow\" as string]: BG_SHADOW,\n      }}\n    >\n      {/* Rotate wrapper - handles 3D rotation on hover */}\n      <div\n        className={cn(\n          \"relative w-fit\",\n          \"[transform-style:preserve-3d]\",\n          \"[container-type:inline-size]\",\n          !shouldReduceMotion && [\n            \"transition-transform duration-[250ms] ease-out\",\n            \"hover:[transform:rotateY(-20deg)_scale(1.066)_translateX(-8px)]\",\n          ]\n        )}\n        style={{\n          aspectRatio: \"49 / 60\",\n          minWidth: bookWidth,\n        }}\n      >\n        {/* Front cover */}\n        <div\n          className={cn(\n            \"absolute flex flex-col\",\n            \"overflow-hidden\",\n            \"[transform:translateZ(0px)]\",\n            \"shadow-[rgba(0,0,0,0.02)_0px_1px_1px,rgba(0,0,0,0.1)_0px_4px_8px_-4px,rgba(0,0,0,0.03)_0px_16px_24px_-8px]\",\n            \"dark:bg-[linear-gradient(rgba(255,255,255,0.1)_0%,rgba(255,255,255,0)_50%,rgba(255,255,255,0)_100%),rgb(31,31,31)]\",\n            \"dark:shadow-[rgba(0,0,0,0.05)_0px_1.8px_3.6px,rgba(0,0,0,0.08)_0px_10.8px_21.6px,rgba(0,0,0,0.1)_0px_-0.9px_inset,rgba(255,255,255,0.1)_0px_1.8px_1.8px_inset,rgba(0,0,0,0.1)_3.6px_0px_3.6px_inset]\"\n          )}\n          style={{\n            background:\n              variant === \"simple\" ? color : \"var(--color-background)\",\n            borderRadius: BOOK_BORDER_RADIUS,\n            height: \"100%\",\n            width: bookWidth,\n          }}\n        >\n          {variant === \"stripe\" ? (\n            <StripeContent\n              illustration={illustration}\n              logo={logo}\n              title={title}\n              width={width}\n            />\n          ) : (\n            <SimpleContent logo={logo} title={title} width={width} />\n          )}\n\n          {/* Border overlay */}\n          <div\n            aria-hidden\n            className={cn(\n              \"pointer-events-none absolute inset-0\",\n              \"border border-black/10\",\n              \"shadow-[rgba(255,255,255,0.3)_0px_1px_2px_inset]\",\n              \"dark:border-0\"\n            )}\n            style={{ borderRadius: \"inherit\" }}\n          />\n        </div>\n\n        {/* Page edges (right side) */}\n        <div\n          aria-hidden\n          className={cn(\n            \"pointer-events-none absolute\",\n            \"bg-[linear-gradient(90deg,rgb(234,234,234)_0%,rgba(0,0,0,0)_70%),linear-gradient(rgb(255,255,255),rgb(250,250,250))]\",\n            \"dark:bg-[linear-gradient(90deg,rgb(60,60,60)_0%,rgba(0,0,0,0)_70%),linear-gradient(rgb(40,40,40),rgb(35,35,35))]\"\n          )}\n          style={{\n            height: \"calc(100% - 6px)\",\n            top: \"3px\",\n            transform: `translateX(calc(${bookWidth} - ${BOOK_DEPTH} / 2 - 3px)) rotateY(90deg) translateX(calc(${BOOK_DEPTH} / 2))`,\n            width: `calc(${BOOK_DEPTH} - 2px)`,\n          }}\n        />\n\n        {/* Back cover */}\n        <div\n          aria-hidden\n          className=\"pointer-events-none absolute left-0\"\n          style={{\n            backgroundColor:\n              variant === \"simple\" ? color : \"var(--color-background)\",\n            borderRadius: BOOK_BORDER_RADIUS,\n            height: \"100%\",\n            transform: `translateZ(calc(-1 * ${BOOK_DEPTH}))`,\n            width: bookWidth,\n          }}\n        />\n      </div>\n    </div>\n  );\n};\n\n/** Stripe variant: color band at top, white body below */\nconst StripeContent = ({\n  title,\n  illustration,\n  logo,\n  width,\n}: {\n  title: string;\n  illustration?: ReactNode;\n  logo?: ReactNode;\n  width: number;\n}) => (\n  <>\n    {/* Colored stripe area */}\n    <div\n      className=\"relative flex w-full flex-1 flex-row items-stretch gap-2 overflow-hidden [transform:translateZ(0px)]\"\n      style={{ background: \"var(--book-color)\" }}\n    >\n      {illustration ? (\n        <div className=\"flex-1 object-cover\">{illustration}</div>\n      ) : (\n        <div className=\"flex-1\" />\n      )}\n      {/* Spine binding overlay on stripe */}\n      <div\n        aria-hidden\n        className=\"absolute inset-0\"\n        style={{\n          background: \"var(--bg-shadow)\",\n          mixBlendMode: \"overlay\",\n        }}\n      />\n    </div>\n\n    {/* Body with title */}\n    <div className=\"flex flex-row items-stretch\">\n      {/* Spine binding on body */}\n      <div\n        aria-hidden\n        className=\"min-w-[8.2%] opacity-20\"\n        style={{ background: \"var(--bg-shadow)\" }}\n      />\n      {/* Content */}\n      <div\n        className=\"flex w-full flex-col justify-between [container-type:inline-size]\"\n        style={{\n          gap: `${(24 / DEFAULT_WIDTH) * width}px`,\n          padding: \"6.1%\",\n        }}\n      >\n        <span\n          className=\"text-balance font-semibold leading-[1.25em] tracking-[-0.02em]\"\n          style={{\n            color: \"var(--book-text-color)\",\n            fontSize: \"10.5cqw\",\n          }}\n        >\n          {title}\n        </span>\n        {logo ? <div>{logo}</div> : null}\n      </div>\n    </div>\n  </>\n);\n\n/** Simple variant: full-color cover with title */\nconst SimpleContent = ({\n  title,\n  logo,\n  width,\n}: {\n  title: string;\n  logo?: ReactNode;\n  width: number;\n}) => (\n  <div className=\"flex h-full w-full flex-row items-stretch\">\n    {/* Spine binding */}\n    <div\n      aria-hidden\n      className=\"min-w-[8.2%]\"\n      style={{\n        background: \"var(--bg-shadow)\",\n        mixBlendMode: \"overlay\",\n        opacity: 1,\n      }}\n    />\n    {/* Content */}\n    <div\n      className=\"flex w-full flex-col justify-between [container-type:inline-size]\"\n      style={{\n        gap: `${(16 / DEFAULT_WIDTH) * width}px`,\n        padding: \"6.1%\",\n      }}\n    >\n      <span\n        className=\"text-balance font-semibold leading-[1.25em] tracking-[-0.02em]\"\n        style={{\n          color: \"var(--book-text-color)\",\n          fontSize: \"12cqw\",\n        }}\n      >\n        {title}\n      </span>\n      {logo ? <div>{logo}</div> : null}\n    </div>\n  </div>\n);\n\nexport default Book;\n","path":"index.tsx","target":"components/smoothui/book/index.tsx","type":"registry:ui"}],"name":"book","registryDependencies":[],"title":"Book","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"A BottomUpLetters text animation component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { motion, useInView, useReducedMotion } from \"motion/react\";\nimport { useRef } from \"react\";\n\nexport interface BottomUpLettersProps {\n  children: string;\n  className?: string;\n  /** Delay before the animation starts, in milliseconds. */\n  delay?: number;\n  /** Per-character stagger, in milliseconds. */\n  stagger?: number;\n  /** Animate only once the text scrolls into view. */\n  triggerOnView?: boolean;\n}\n\nconst DURATION_S = 0.4;\nconst MS = 1000;\n// Pronounced ease-out for a tall, staged lift from below.\nconst EASE = [0.18, 1, 0.32, 1] as const;\n\n/**\n * BottomUpLetters — letters rise from below in a pronounced staircase,\n * one symbol at a time, with zero blur. Best for short single words or\n * compact headlines at 40px+. From the animate-text catalog (`bottom-up-letters`).\n */\nexport default function BottomUpLetters({\n  children,\n  className = \"\",\n  delay = 0,\n  stagger = 88,\n  triggerOnView = false,\n}: BottomUpLettersProps) {\n  const ref = useRef<HTMLSpanElement>(null);\n  const inView = useInView(ref, { once: true });\n  const shouldReduceMotion = useReducedMotion();\n  const play = (!triggerOnView || inView) && !shouldReduceMotion;\n  const characters = Array.from(children);\n\n  return (\n    <span aria-label={children} className={className} ref={ref}>\n      {characters.map((char, index) => (\n        <motion.span\n          animate={play ? { opacity: 1, y: 0 } : undefined}\n          aria-hidden=\"true\"\n          initial={shouldReduceMotion ? { opacity: 1 } : { opacity: 0, y: 46 }}\n          // biome-ignore lint/suspicious/noArrayIndexKey: characters have no stable id\n          key={index}\n          style={{ display: \"inline-block\", whiteSpace: \"pre\" }}\n          transition={\n            shouldReduceMotion\n              ? { duration: 0 }\n              : {\n                  delay: delay / MS + (index * stagger) / MS,\n                  duration: DURATION_S,\n                  ease: EASE,\n                }\n          }\n        >\n          {char === \" \" ? \" \" : String(char)}\n        </motion.span>\n      ))}\n    </span>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/bottom-up-letters/index.tsx","type":"registry:ui"}],"name":"bottom-up-letters","registryDependencies":[],"title":"Bottom Up Letters","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion","lucide-react"],"description":"Animated breadcrumb navigation with stagger-in animation","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { ChevronRight } from \"lucide-react\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport type { ReactNode } from \"react\";\nimport { SPRING_DEFAULT } from \"@/components/smoothui/lib/animation\";\n\n/** A single breadcrumb item definition. */\nexport type BreadcrumbItemProps = {\n  /** Display label for the breadcrumb item. */\n  label: ReactNode;\n  /** URL the breadcrumb links to. Omit for the current (last) page. */\n  href?: string;\n};\n\nexport type BreadcrumbProps = {\n  /** Ordered list of breadcrumb items. The last item is treated as the current page. */\n  items: BreadcrumbItemProps[];\n  /** Custom separator element. Defaults to a chevron icon. */\n  separator?: ReactNode;\n  /** Additional CSS classes for the nav element. */\n  className?: string;\n};\n\nconst staggerDelay = 0.04;\n\nexport default function Breadcrumb({\n  items,\n  separator,\n  className,\n}: BreadcrumbProps) {\n  const shouldReduceMotion = useReducedMotion();\n\n  return (\n    <nav aria-label=\"Breadcrumb\" className={cn(\"inline-flex\", className)}>\n      <ol className=\"flex flex-wrap items-center gap-1.5 text-muted-foreground text-sm sm:gap-2.5\">\n        {items.map((item, index) => {\n          const isLast = index === items.length - 1;\n\n          return (\n            <motion.li\n              animate={{ opacity: 1, transform: \"translateX(0px)\" }}\n              className=\"inline-flex items-center gap-1.5\"\n              initial={\n                shouldReduceMotion\n                  ? { opacity: 1 }\n                  : { opacity: 0, transform: \"translateX(-4px)\" }\n              }\n              key={\n                typeof item.label === \"string\"\n                  ? item.label\n                  : `breadcrumb-${String(index)}`\n              }\n              transition={\n                shouldReduceMotion\n                  ? { duration: 0 }\n                  : {\n                      ...SPRING_DEFAULT,\n                      delay: index * staggerDelay,\n                    }\n              }\n            >\n              {index > 0 && (\n                <span\n                  aria-hidden=\"true\"\n                  className=\"mr-1.5 [&>svg]:size-3.5\"\n                  role=\"presentation\"\n                >\n                  {separator ?? <ChevronRight className=\"size-3.5\" />}\n                </span>\n              )}\n              {isLast ? (\n                <span\n                  aria-current=\"page\"\n                  className=\"font-normal text-foreground\"\n                >\n                  {item.label}\n                </span>\n              ) : (\n                <a\n                  className=\"transition-colors hover:text-foreground\"\n                  href={item.href ?? \"#\"}\n                >\n                  {item.label}\n                </a>\n              )}\n            </motion.li>\n          );\n        })}\n      </ol>\n    </nav>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/breadcrumb/index.tsx","type":"registry:ui"}],"name":"breadcrumb","registryDependencies":["https://smoothui.dev/r/lib.json"],"title":"Breadcrumb","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion","lucide-react"],"description":"A ButtonCopy component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { Check, Copy, LoaderCircle } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { type ReactNode, useCallback, useState } from \"react\";\n\nexport interface ButtonCopyProps {\n  className?: string;\n  disabled?: boolean;\n  duration?: number;\n  idleIcon?: ReactNode;\n  loadingDuration?: number;\n  loadingIcon?: ReactNode;\n  onCopy?: () => Promise<void> | void;\n  successIcon?: ReactNode;\n}\n\nconst defaultIcons = {\n  idle: <Copy size={16} />,\n  loading: <LoaderCircle className=\"animate-spin\" size={16} />,\n  success: <Check size={16} />,\n};\n\nexport default function ButtonCopy({\n  onCopy,\n  idleIcon = defaultIcons.idle,\n  loadingIcon = defaultIcons.loading,\n  successIcon = defaultIcons.success,\n  className = \"\",\n  duration = 2000,\n  loadingDuration = 1000,\n  disabled = false,\n}: ButtonCopyProps) {\n  const [buttonState, setButtonState] = useState<\n    \"idle\" | \"loading\" | \"success\"\n  >(\"idle\");\n  const shouldReduceMotion = useReducedMotion();\n\n  const handleClick = useCallback(async () => {\n    setButtonState(\"loading\");\n    if (onCopy) {\n      await onCopy();\n    }\n    setTimeout(() => {\n      setButtonState(\"success\");\n    }, loadingDuration);\n    setTimeout(() => {\n      setButtonState(\"idle\");\n    }, loadingDuration + duration);\n  }, [onCopy, loadingDuration, duration]);\n\n  const icons = {\n    idle: idleIcon,\n    loading: loadingIcon,\n    success: successIcon,\n  };\n\n  const ariaLabels = {\n    idle: \"Copy\",\n    loading: \"Copying...\",\n    success: \"Copied\",\n  };\n\n  return (\n    <div className=\"flex justify-center\">\n      <button\n        aria-label={ariaLabels[buttonState]}\n        aria-live=\"polite\"\n        className={`relative min-h-[44px] w-auto min-w-[44px] cursor-pointer overflow-hidden rounded-full border bg-background p-3 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 ${className}`}\n        disabled={buttonState !== \"idle\" || disabled}\n        onClick={handleClick}\n        type=\"button\"\n      >\n        <AnimatePresence initial={false} mode=\"popLayout\">\n          <motion.span\n            animate={\n              shouldReduceMotion\n                ? { opacity: 1 }\n                : { filter: \"blur(0px)\", opacity: 1, y: 0 }\n            }\n            className=\"flex w-full items-center justify-center\"\n            exit={\n              shouldReduceMotion\n                ? { opacity: 0, transition: { duration: 0 } }\n                : { filter: \"blur(10px)\", opacity: 0, y: 25 }\n            }\n            initial={\n              shouldReduceMotion\n                ? { opacity: 1 }\n                : { filter: \"blur(10px)\", opacity: 0, y: -25 }\n            }\n            key={buttonState}\n            transition={\n              shouldReduceMotion\n                ? { duration: 0 }\n                : { bounce: 0, duration: 0.25, type: \"spring\" as const }\n            }\n          >\n            {icons[buttonState]}\n          </motion.span>\n        </AnimatePresence>\n      </button>\n    </div>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/button-copy/index.tsx","type":"registry:ui"}],"name":"button-copy","registryDependencies":[],"title":"Button Copy","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion","radix-ui"],"description":"An animated Checkbox component for SmoothUI with checkmark and indeterminate state animations.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { Checkbox as CheckboxPrimitive } from \"radix-ui\";\nimport { SPRING_DEFAULT } from \"@/components/smoothui/lib/animation\";\n\nexport interface CheckboxProps {\n  /** Whether the checkbox is checked */\n  checked?: boolean;\n  /** Optional CSS class */\n  className?: string;\n  /** Whether the checkbox is disabled */\n  disabled?: boolean;\n  /** ID for label association */\n  id?: string;\n  /** Whether the checkbox is in an indeterminate state */\n  indeterminate?: boolean;\n  /** Accessible name for the checkbox */\n  name?: string;\n  /** Callback when the checked state changes */\n  onCheckedChange?: (checked: boolean) => void;\n  /** Whether the checkbox is required */\n  required?: boolean;\n  /** Value attribute for form submission */\n  value?: string;\n}\n\nconst CheckmarkPath = motion.path;\nconst MotionSvg = motion.svg;\n\nexport default function Checkbox({\n  checked = false,\n  indeterminate = false,\n  onCheckedChange,\n  disabled = false,\n  className,\n  name,\n  value,\n  id,\n  required,\n}: CheckboxProps) {\n  const shouldReduceMotion = useReducedMotion();\n\n  const derivedState = indeterminate\n    ? \"indeterminate\"\n    : checked\n      ? \"checked\"\n      : \"unchecked\";\n\n  const handleChange = (state: boolean | \"indeterminate\") => {\n    if (state === \"indeterminate\") {\n      return;\n    }\n    onCheckedChange?.(state);\n  };\n\n  return (\n    <CheckboxPrimitive.Root\n      aria-checked={indeterminate ? \"mixed\" : checked}\n      checked={indeterminate ? \"indeterminate\" : checked}\n      className={cn(\n        \"peer size-4 shrink-0 rounded-[4px] border border-input shadow-xs outline-none transition-shadow focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=checked]:border-foreground data-[state=indeterminate]:border-foreground data-[state=checked]:bg-foreground data-[state=indeterminate]:bg-foreground data-[state=unchecked]:bg-background data-[state=checked]:text-background data-[state=indeterminate]:text-background dark:data-[state=unchecked]:bg-input/30 dark:aria-invalid:ring-destructive/40\",\n        className\n      )}\n      data-slot=\"checkbox\"\n      disabled={disabled}\n      id={id}\n      name={name}\n      onCheckedChange={handleChange}\n      required={required}\n      value={value}\n    >\n      <CheckboxPrimitive.Indicator\n        className=\"grid place-content-center text-current\"\n        data-slot=\"checkbox-indicator\"\n        forceMount\n      >\n        <AnimatePresence mode=\"wait\">\n          {derivedState === \"checked\" && (\n            <MotionSvg\n              animate={\n                shouldReduceMotion ? { opacity: 1 } : { opacity: 1, scale: 1 }\n              }\n              className=\"size-3.5\"\n              exit={\n                shouldReduceMotion\n                  ? { opacity: 0, transition: { duration: 0 } }\n                  : { opacity: 0, scale: 0.8 }\n              }\n              fill=\"none\"\n              initial={\n                shouldReduceMotion ? { opacity: 1 } : { opacity: 0, scale: 0.8 }\n              }\n              key=\"check\"\n              stroke=\"currentColor\"\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n              strokeWidth={3}\n              transition={shouldReduceMotion ? { duration: 0 } : SPRING_DEFAULT}\n              viewBox=\"0 0 24 24\"\n            >\n              <title>Checked</title>\n              <CheckmarkPath\n                animate={shouldReduceMotion ? {} : { pathLength: 1 }}\n                d=\"M20 6L9 17l-5-5\"\n                initial={shouldReduceMotion ? {} : { pathLength: 0 }}\n                transition={\n                  shouldReduceMotion\n                    ? { duration: 0 }\n                    : { ...SPRING_DEFAULT, delay: 0.05 }\n                }\n              />\n            </MotionSvg>\n          )}\n          {derivedState === \"indeterminate\" && (\n            <MotionSvg\n              animate={\n                shouldReduceMotion ? { opacity: 1 } : { opacity: 1, scale: 1 }\n              }\n              className=\"size-3.5\"\n              exit={\n                shouldReduceMotion\n                  ? { opacity: 0, transition: { duration: 0 } }\n                  : { opacity: 0, scale: 0.8 }\n              }\n              fill=\"none\"\n              initial={\n                shouldReduceMotion ? { opacity: 1 } : { opacity: 0, scale: 0.8 }\n              }\n              key=\"indeterminate\"\n              stroke=\"currentColor\"\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n              strokeWidth={3}\n              transition={shouldReduceMotion ? { duration: 0 } : SPRING_DEFAULT}\n              viewBox=\"0 0 24 24\"\n            >\n              <title>Indeterminate</title>\n              <CheckmarkPath\n                animate={shouldReduceMotion ? {} : { pathLength: 1 }}\n                d=\"M5 12h14\"\n                initial={shouldReduceMotion ? {} : { pathLength: 0 }}\n                transition={\n                  shouldReduceMotion\n                    ? { duration: 0 }\n                    : { ...SPRING_DEFAULT, delay: 0.05 }\n                }\n              />\n            </MotionSvg>\n          )}\n        </AnimatePresence>\n      </CheckboxPrimitive.Indicator>\n    </CheckboxPrimitive.Root>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/checkbox/index.tsx","type":"registry:ui"}],"name":"checkbox","registryDependencies":["https://smoothui.dev/r/lib.json"],"title":"Checkbox","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"Persistent WebGL shader transition wrapper that swaps content under a soft chromatic blur wash.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { useReducedMotion } from \"motion/react\";\nimport { type ReactNode, useEffect, useRef, useState } from \"react\";\n\nexport interface ChromaBlurTransitionProps {\n  children: ReactNode;\n  className?: string;\n  duration?: number;\n  onRest?: () => void;\n  transitionKey: string | number;\n}\n\nconst DEFAULT_DURATION_MS = 1040;\nconst MIDPOINT = 0.5;\nconst MAX_DPR = 2;\nconst VERTEX_SHADER =\n  \"attribute vec2 a; void main(){ gl_Position = vec4(a, 0.0, 1.0); }\";\nconst FRAGMENT_SHADER = `\nprecision mediump float;\nuniform vec2 uRes;\nuniform float uTime;\nuniform float uProgress;\nuniform float uAlpha;\n#define PI 3.14159265359\n\nfloat hash(vec2 p){ return fract(sin(dot(p, vec2(41.0, 289.0))) * 45758.5453); }\nfloat noise(vec2 p){\n  vec2 i = floor(p);\n  vec2 f = fract(p);\n  float a = hash(i);\n  float b = hash(i + vec2(1.0, 0.0));\n  float c = hash(i + vec2(0.0, 1.0));\n  float d = hash(i + vec2(1.0, 1.0));\n  vec2 u = f * f * (3.0 - 2.0 * f);\n  return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y;\n}\n\nvoid main(){\n  vec2 uv = gl_FragCoord.xy / uRes;\n  float p = smoothstep(0.0, 1.0, uProgress);\n  float field = uv.x * 0.62 + uv.y * 0.38;\n  float n = noise(uv * 10.0 + vec2(uTime * 0.28, -uTime * 0.18));\n  float wave = sin((uv.y + n * 0.12) * 28.0 + uTime * 2.2) * 0.018;\n  float front = p * 1.42 - 0.2;\n  float d = field + wave - front;\n\n  float curtain = exp(-d * d * 34.0);\n  float softGrain = noise(uv * 34.0 + uTime * 0.18) * 0.055;\n  float chroma = exp(-(d + 0.045) * (d + 0.045) * 52.0) + exp(-(d - 0.045) * (d - 0.045) * 52.0);\n  float exitFade = smoothstep(1.0, 0.74, p);\n  float entryFade = smoothstep(0.0, 0.16, p);\n\n  vec3 brandPink = vec3(1.0, 0.32, 0.64);\n  vec3 champagne = vec3(1.0, 0.82, 0.56);\n  vec3 seafoam = vec3(0.52, 0.90, 0.78);\n  vec3 porcelain = vec3(1.0, 0.97, 0.90);\n  vec3 color = mix(brandPink, champagne, smoothstep(-0.18, 0.18, sin((uv.y + n) * PI * 2.0)));\n  color = mix(color, seafoam, smoothstep(0.2, 1.0, chroma) * 0.34);\n  color = mix(color, porcelain, 0.16);\n\n  float alpha = (curtain * 0.72 + chroma * 0.18 + softGrain * curtain) * entryFade * exitFade * uAlpha;\n  gl_FragColor = vec4(color * alpha, clamp(alpha, 0.0, 0.86));\n}\n`;\nconst STRIP = new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]);\n\ninterface Playback {\n  cancel: () => void;\n}\ninterface Controller {\n  destroy: () => void;\n  setAlpha: (alpha: number) => void;\n  setProgress: (progress: number) => void;\n}\n\nconst clamp01 = (value: number) => Math.max(0, Math.min(1, value));\nconst easeOutQuart = (value: number) => 1 - (1 - value) ** 4;\nconst easeInOutCubic = (value: number) =>\n  value < 0.5 ? 4 * value ** 3 : 1 - (-2 * value + 2) ** 3 / 2;\n\nconst compile = (gl: WebGLRenderingContext, type: number, source: string) => {\n  const shader = gl.createShader(type);\n  if (!shader) {\n    return null;\n  }\n  gl.shaderSource(shader, source);\n  gl.compileShader(shader);\n  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n    gl.deleteShader(shader);\n    return null;\n  }\n  return shader;\n};\n\nconst resize = (canvas: HTMLCanvasElement, gl: WebGLRenderingContext) => {\n  const rect = canvas.getBoundingClientRect();\n  const dpr = Math.min(window.devicePixelRatio || 1, MAX_DPR);\n  const width = Math.max(1, Math.round(rect.width * dpr));\n  const height = Math.max(1, Math.round(rect.height * dpr));\n  if (canvas.width !== width || canvas.height !== height) {\n    canvas.width = width;\n    canvas.height = height;\n  }\n  gl.viewport(0, 0, width, height);\n};\n\nconst createController = (canvas: HTMLCanvasElement): Controller | null => {\n  const gl = canvas.getContext(\"webgl\", {\n    alpha: true,\n    antialias: false,\n    premultipliedAlpha: true,\n  });\n  if (!gl) {\n    return null;\n  }\n  const vertex = compile(gl, gl.VERTEX_SHADER, VERTEX_SHADER);\n  const fragment = compile(gl, gl.FRAGMENT_SHADER, FRAGMENT_SHADER);\n  const program = gl.createProgram();\n  if (!(vertex && fragment && program)) {\n    return null;\n  }\n  gl.attachShader(program, vertex);\n  gl.attachShader(program, fragment);\n  gl.linkProgram(program);\n  gl.deleteShader(vertex);\n  gl.deleteShader(fragment);\n  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n    gl.deleteProgram(program);\n    return null;\n  }\n\n  // biome-ignore lint/correctness/useHookAtTopLevel: WebGLRenderingContext.useProgram is not a React hook.\n  gl.useProgram(program);\n  const buffer = gl.createBuffer();\n  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);\n  gl.bufferData(gl.ARRAY_BUFFER, STRIP, gl.STATIC_DRAW);\n  const position = gl.getAttribLocation(program, \"a\");\n  gl.enableVertexAttribArray(position);\n  gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0);\n  gl.enable(gl.BLEND);\n  gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);\n\n  const uRes = gl.getUniformLocation(program, \"uRes\");\n  const uTime = gl.getUniformLocation(program, \"uTime\");\n  const uProgress = gl.getUniformLocation(program, \"uProgress\");\n  const uAlpha = gl.getUniformLocation(program, \"uAlpha\");\n  const startedAt = performance.now();\n  let progress = 0;\n  let alpha = 0;\n  let frame = 0;\n  let destroyed = false;\n\n  const tick = () => {\n    if (destroyed) {\n      return;\n    }\n    resize(canvas, gl);\n    gl.clearColor(0, 0, 0, 0);\n    gl.clear(gl.COLOR_BUFFER_BIT);\n    gl.uniform2f(uRes, canvas.width, canvas.height);\n    gl.uniform1f(uTime, (performance.now() - startedAt) / 1000);\n    gl.uniform1f(uProgress, progress);\n    gl.uniform1f(uAlpha, alpha);\n    gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);\n    frame = requestAnimationFrame(tick);\n  };\n  frame = requestAnimationFrame(tick);\n\n  return {\n    destroy: () => {\n      destroyed = true;\n      cancelAnimationFrame(frame);\n      gl.clear(gl.COLOR_BUFFER_BIT);\n      if (buffer) {\n        gl.deleteBuffer(buffer);\n      }\n      gl.deleteProgram(program);\n    },\n    setAlpha: (nextAlpha: number) => {\n      alpha = clamp01(nextAlpha);\n    },\n    setProgress: (nextProgress: number) => {\n      progress = clamp01(nextProgress);\n    },\n  };\n};\n\nconst play = (\n  controller: Controller,\n  duration: number,\n  onMidpoint: () => void,\n  onComplete: () => void\n): Playback => {\n  let frame = 0;\n  let cancelled = false;\n  let didSwap = false;\n  const startedAt = performance.now();\n  controller.setAlpha(1);\n\n  const advance = () => {\n    if (cancelled) {\n      return;\n    }\n    const raw = clamp01((performance.now() - startedAt) / duration);\n    const progress = easeOutQuart(raw);\n    controller.setProgress(progress);\n    if (!(didSwap || progress < MIDPOINT)) {\n      didSwap = true;\n      onMidpoint();\n    }\n    if (raw < 1) {\n      frame = requestAnimationFrame(advance);\n      return;\n    }\n    if (!didSwap) {\n      onMidpoint();\n    }\n    const fadeStartedAt = performance.now();\n    const fade = () => {\n      if (cancelled) {\n        return;\n      }\n      const fadeRaw = clamp01((performance.now() - fadeStartedAt) / 90);\n      controller.setAlpha(1 - easeInOutCubic(fadeRaw));\n      if (fadeRaw < 1) {\n        frame = requestAnimationFrame(fade);\n        return;\n      }\n      controller.setAlpha(0);\n      controller.setProgress(0);\n      onComplete();\n    };\n    frame = requestAnimationFrame(fade);\n  };\n  frame = requestAnimationFrame(advance);\n\n  return {\n    cancel: () => {\n      cancelled = true;\n      cancelAnimationFrame(frame);\n    },\n  };\n};\n\nconst ChromaBlurTransition = ({\n  children,\n  className,\n  duration = DEFAULT_DURATION_MS,\n  onRest,\n  transitionKey,\n}: ChromaBlurTransitionProps) => {\n  const shouldReduceMotion = useReducedMotion();\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const controllerRef = useRef<Controller | null>(null);\n  const playbackRef = useRef<Playback | null>(null);\n  const previousKey = useRef(transitionKey);\n  const [renderedChildren, setRenderedChildren] = useState(children);\n  const [isActive, setIsActive] = useState(false);\n\n  useEffect(() => {\n    const canvas = canvasRef.current;\n    if (!canvas) {\n      return;\n    }\n    const controller = createController(canvas);\n    controllerRef.current = controller;\n    return () => {\n      playbackRef.current?.cancel();\n      controller?.destroy();\n      controllerRef.current = null;\n    };\n  }, []);\n\n  useEffect(() => {\n    if (previousKey.current === transitionKey) {\n      setRenderedChildren(children);\n      return;\n    }\n    previousKey.current = transitionKey;\n    playbackRef.current?.cancel();\n\n    if (shouldReduceMotion) {\n      setRenderedChildren(children);\n      setIsActive(false);\n      onRest?.();\n      return;\n    }\n\n    const controller = controllerRef.current;\n    if (!controller) {\n      setRenderedChildren(children);\n      onRest?.();\n      return;\n    }\n    setIsActive(true);\n    playbackRef.current = play(\n      controller,\n      duration,\n      () => setRenderedChildren(children),\n      () => {\n        setIsActive(false);\n        playbackRef.current = null;\n        onRest?.();\n      }\n    );\n  }, [children, duration, onRest, shouldReduceMotion, transitionKey]);\n\n  return (\n    <div\n      className={cn(\n        \"relative isolate overflow-hidden rounded-2xl bg-background text-foreground\",\n        className\n      )}\n      data-transitioning={isActive ? \"true\" : \"false\"}\n    >\n      <div className=\"relative z-0\">{renderedChildren}</div>\n      <canvas\n        className={cn(\n          \"pointer-events-none absolute inset-0 z-10 h-full w-full transition-opacity duration-75\",\n          isActive && !shouldReduceMotion ? \"opacity-100\" : \"opacity-0\"\n        )}\n        ref={canvasRef}\n      />\n    </div>\n  );\n};\n\nexport default ChromaBlurTransition;\n","path":"index.tsx","target":"components/smoothui/chroma-blur-transition/index.tsx","type":"registry:ui"}],"name":"chroma-blur-transition","registryDependencies":[],"title":"Chroma Blur Transition","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"A ClipCornersButton component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport SmoothButton from \"@/components/smoothui/smooth-button\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { useEffect, useState } from \"react\";\n\nexport interface ClipCornersButtonProps {\n  children: React.ReactNode;\n  className?: string;\n  onClick?: () => void;\n}\n\nexport function ClipCornersButton({\n  children,\n  className = \"\",\n  onClick,\n}: ClipCornersButtonProps) {\n  const [isHovered, setIsHovered] = useState(false);\n  const shouldReduceMotion = useReducedMotion();\n  const [isHoverDevice, setIsHoverDevice] = useState(false);\n\n  // Distance triangles move on hover\n  const move = -4;\n\n  useEffect(() => {\n    const mediaQuery = window.matchMedia(\"(hover: hover) and (pointer: fine)\");\n    setIsHoverDevice(mediaQuery.matches);\n\n    const handleChange = (e: MediaQueryListEvent) => {\n      setIsHoverDevice(e.matches);\n    };\n\n    mediaQuery.addEventListener(\"change\", handleChange);\n    return () => mediaQuery.removeEventListener(\"change\", handleChange);\n  }, []);\n\n  return (\n    <SmoothButton\n      className={`relative overflow-hidden rounded-none border-none bg-foreground px-8 py-4 font-mono text-2xl text-background hover:bg-foreground/90 ${className}`}\n      onClick={onClick}\n      onMouseEnter={() => {\n        if (isHoverDevice) {\n          setIsHovered(true);\n        }\n      }}\n      onMouseLeave={() => setIsHovered(false)}\n      style={{ borderRadius: 8 }}\n      type=\"button\"\n    >\n      {/* Top-left triangle */}\n      <motion.div\n        animate={\n          shouldReduceMotion || !isHoverDevice\n            ? {}\n            : {\n                x: isHovered ? -move : 0,\n                y: isHovered ? -move : 0,\n              }\n        }\n        className=\"absolute top-1.5 left-1.5\"\n        initial={false}\n        style={{ height: 8, width: 8 }}\n        transition={\n          shouldReduceMotion\n            ? { duration: 0 }\n            : {\n                damping: 24,\n                duration: 0.2,\n                stiffness: 400,\n                type: \"spring\" as const,\n              }\n        }\n      >\n        <svg\n          aria-label=\"Top-left triangle\"\n          className=\"fill-background\"\n          height=\"8\"\n          width=\"8\"\n        >\n          <title>Top-left triangle</title>\n          <polygon fill=\"currentColor\" points=\"0,0 8,0 0,8\" />\n        </svg>\n      </motion.div>\n      {/* Top-right triangle */}\n      <motion.div\n        animate={\n          shouldReduceMotion || !isHoverDevice\n            ? {}\n            : {\n                x: isHovered ? move : 0,\n                y: isHovered ? -move : 0,\n              }\n        }\n        className=\"absolute top-1.5 right-1.5\"\n        initial={false}\n        style={{ height: 8, width: 8 }}\n        transition={\n          shouldReduceMotion\n            ? { duration: 0 }\n            : {\n                damping: 24,\n                duration: 0.2,\n                stiffness: 400,\n                type: \"spring\" as const,\n              }\n        }\n      >\n        <svg\n          aria-label=\"Top-right triangle\"\n          className=\"fill-background\"\n          height=\"8\"\n          width=\"8\"\n        >\n          <title>Top-right triangle</title>\n          <polygon fill=\"currentColor\" points=\"8,0 8,8 0,0\" />\n        </svg>\n      </motion.div>\n      {/* Bottom-left triangle */}\n      <motion.div\n        animate={\n          shouldReduceMotion || !isHoverDevice\n            ? {}\n            : {\n                x: isHovered ? -move : 0,\n                y: isHovered ? move : 0,\n              }\n        }\n        className=\"absolute bottom-1.5 left-1.5\"\n        initial={false}\n        style={{ height: 8, width: 8 }}\n        transition={\n          shouldReduceMotion\n            ? { duration: 0 }\n            : {\n                damping: 24,\n                duration: 0.2,\n                stiffness: 400,\n                type: \"spring\" as const,\n              }\n        }\n      >\n        <svg\n          aria-label=\"Bottom-left triangle\"\n          className=\"fill-background\"\n          height=\"8\"\n          width=\"8\"\n        >\n          <title>Bottom-left triangle</title>\n          <polygon fill=\"currentColor\" points=\"0,8 8,8 0,0\" />\n        </svg>\n      </motion.div>\n      {/* Bottom-right triangle */}\n      <motion.div\n        animate={\n          shouldReduceMotion || !isHoverDevice\n            ? {}\n            : {\n                x: isHovered ? move : 0,\n                y: isHovered ? move : 0,\n              }\n        }\n        className=\"absolute right-1.5 bottom-1.5\"\n        initial={false}\n        style={{ height: 8, width: 8 }}\n        transition={\n          shouldReduceMotion\n            ? { duration: 0 }\n            : {\n                damping: 24,\n                duration: 0.2,\n                stiffness: 400,\n                type: \"spring\" as const,\n              }\n        }\n      >\n        <svg\n          aria-label=\"Bottom-right triangle\"\n          className=\"fill-background\"\n          height=\"8\"\n          width=\"8\"\n        >\n          <title>Bottom-right triangle</title>\n          <polygon fill=\"currentColor\" points=\"8,8 0,8 8,0\" />\n        </svg>\n      </motion.div>\n      <span className=\"relative z-10 flex w-full select-none items-center justify-center\">\n        {children}\n      </span>\n    </SmoothButton>\n  );\n}\n\nexport default ClipCornersButton;\n","path":"index.tsx","target":"components/smoothui/clip-corners-button/index.tsx","type":"registry:ui"}],"name":"clip-corners-button","registryDependencies":["https://smoothui.dev/r/smooth-button.json"],"title":"Clip Corners Button","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion","lucide-react"],"description":"An animated Combobox component for SmoothUI with text filtering, async search, and keyboard navigation.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport {\n  Command,\n  CommandEmpty,\n  CommandGroup,\n  CommandInput,\n  CommandItem,\n  CommandList,\n} from \"@/components/ui/command\";\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport { cn } from \"@/lib/utils\";\nimport { CheckIcon, ChevronsUpDownIcon, LoaderIcon } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport { DURATION_INSTANT, SPRING_DEFAULT } from \"@/components/smoothui/lib/animation\";\nimport SmoothButton from \"../smooth-button\";\n\nexport interface ComboboxOption {\n  /** Whether the option is disabled */\n  disabled?: boolean;\n  /** The display label for the option */\n  label: string;\n  /** The value of the option */\n  value: string;\n}\n\nexport interface ComboboxProps {\n  /** Accessible label for the combobox */\n  \"aria-label\"?: string;\n  /** ID of element that labels this combobox */\n  \"aria-labelledby\"?: string;\n  /** Additional CSS class names for the trigger button */\n  className?: string;\n  /** Additional CSS class names for the popover content */\n  contentClassName?: string;\n  /** Whether the combobox is disabled */\n  disabled?: boolean;\n  /** Text shown when no results match */\n  emptyText?: string;\n  /** Async search callback — receives the query string, returns filtered options */\n  onSearch?: (query: string) => Promise<ComboboxOption[]>;\n  /** Callback when the selected value changes */\n  onValueChange?: (value: string) => void;\n  /** Static list of options (used when onSearch is not provided) */\n  options?: ComboboxOption[];\n  /** Placeholder text for the trigger button */\n  placeholder?: string;\n  /** Debounce delay in ms for the onSearch callback */\n  searchDebounce?: number;\n  /** Placeholder text for the search input */\n  searchPlaceholder?: string;\n  /** The controlled selected value */\n  value?: string;\n}\n\nconst MotionCommandItem = motion.create(CommandItem);\n\nexport default function Combobox({\n  value,\n  onValueChange,\n  options: staticOptions,\n  onSearch,\n  searchDebounce = 300,\n  placeholder = \"Select an option…\",\n  searchPlaceholder = \"Search…\",\n  emptyText = \"No results found.\",\n  disabled = false,\n  className,\n  contentClassName,\n  \"aria-label\": ariaLabel,\n  \"aria-labelledby\": ariaLabelledBy,\n}: ComboboxProps) {\n  const shouldReduceMotion = useReducedMotion();\n  const [open, setOpen] = useState(false);\n  const [query, setQuery] = useState(\"\");\n  const [asyncOptions, setAsyncOptions] = useState<ComboboxOption[]>([]);\n  const [loading, setLoading] = useState(false);\n  const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n  const displayOptions = onSearch ? asyncOptions : (staticOptions ?? []);\n\n  const selectedLabel = displayOptions.find(\n    (opt) => opt.value === value\n  )?.label;\n\n  const handleSearch = useCallback(\n    (searchQuery: string) => {\n      setQuery(searchQuery);\n\n      if (!onSearch) {\n        return;\n      }\n\n      if (debounceRef.current) {\n        clearTimeout(debounceRef.current);\n      }\n\n      debounceRef.current = setTimeout(async () => {\n        setLoading(true);\n        try {\n          const results = await onSearch(searchQuery);\n          setAsyncOptions(results);\n        } finally {\n          setLoading(false);\n        }\n      }, searchDebounce);\n    },\n    [onSearch, searchDebounce]\n  );\n\n  // Load initial async options when popover opens\n  useEffect(() => {\n    if (open && onSearch && asyncOptions.length === 0 && !loading) {\n      setLoading(true);\n      onSearch(\"\").then((results) => {\n        setAsyncOptions(results);\n        setLoading(false);\n      });\n    }\n  }, [open, onSearch, asyncOptions.length, loading]);\n\n  // Clean up debounce on unmount\n  useEffect(\n    () => () => {\n      if (debounceRef.current) {\n        clearTimeout(debounceRef.current);\n      }\n    },\n    []\n  );\n\n  const handleSelect = (selectedValue: string) => {\n    const newValue = selectedValue === value ? \"\" : selectedValue;\n    onValueChange?.(newValue);\n    setOpen(false);\n  };\n\n  const itemTransition = shouldReduceMotion ? DURATION_INSTANT : SPRING_DEFAULT;\n\n  return (\n    <Popover onOpenChange={setOpen} open={open}>\n      <PopoverTrigger asChild>\n        <SmoothButton\n          aria-expanded={open}\n          aria-haspopup=\"listbox\"\n          aria-label={ariaLabel}\n          aria-labelledby={ariaLabelledBy}\n          className={cn(\n            \"h-9 w-full justify-between px-3 py-2 text-left font-normal [&:hover_*]:text-white\",\n            !selectedLabel && \"text-muted-foreground\",\n            shouldReduceMotion && \"!transition-none !duration-0\",\n            className\n          )}\n          disabled={disabled}\n          role=\"combobox\"\n          type=\"button\"\n          variant=\"outline\"\n        >\n          <span\n            className={cn(\n              \"truncate transition-colors\",\n              !selectedLabel && \"text-muted-foreground\"\n            )}\n          >\n            {selectedLabel ?? placeholder}\n          </span>\n          <ChevronsUpDownIcon className=\"ml-2 size-4 shrink-0 opacity-50 transition-colors\" />\n        </SmoothButton>\n      </PopoverTrigger>\n      <PopoverContent\n        align=\"start\"\n        className={cn(\n          \"w-[var(--radix-popover-trigger-width)] p-0\",\n          shouldReduceMotion && \"!animate-none !transition-none !duration-0\",\n          contentClassName\n        )}\n      >\n        <Command shouldFilter={!onSearch}>\n          <CommandInput\n            className=\"\"\n            onValueChange={handleSearch}\n            placeholder={searchPlaceholder}\n            value={query}\n          />\n          <CommandList>\n            <AnimatePresence>\n              {loading ? (\n                <motion.div\n                  animate={{ opacity: 1 }}\n                  className=\"flex items-center justify-center py-4\"\n                  exit={\n                    shouldReduceMotion\n                      ? { opacity: 0, transition: DURATION_INSTANT }\n                      : { opacity: 0 }\n                  }\n                  initial={shouldReduceMotion ? { opacity: 1 } : { opacity: 0 }}\n                  transition={itemTransition}\n                >\n                  <LoaderIcon className=\"size-4 animate-spin text-muted-foreground\" />\n                  <span className=\"ml-2 text-muted-foreground text-sm\">\n                    Loading…\n                  </span>\n                </motion.div>\n              ) : null}\n            </AnimatePresence>\n\n            {!loading && <CommandEmpty>{emptyText}</CommandEmpty>}\n\n            {!loading && displayOptions.length > 0 && (\n              <CommandGroup>\n                {displayOptions.map((option, index) => (\n                  <MotionCommandItem\n                    animate={{ opacity: 1, transform: \"translateY(0px)\" }}\n                    disabled={option.disabled}\n                    initial={\n                      shouldReduceMotion\n                        ? { opacity: 1 }\n                        : { opacity: 0, transform: \"translateY(4px)\" }\n                    }\n                    key={option.value}\n                    keywords={[option.label]}\n                    onSelect={handleSelect}\n                    transition={\n                      shouldReduceMotion\n                        ? DURATION_INSTANT\n                        : {\n                            ...SPRING_DEFAULT,\n                            delay: index * 0.02,\n                          }\n                    }\n                    value={option.value}\n                  >\n                    <CheckIcon\n                      className={cn(\n                        \"mr-2 size-4 shrink-0\",\n                        value === option.value ? \"opacity-100\" : \"opacity-0\"\n                      )}\n                    />\n                    {option.label}\n                  </MotionCommandItem>\n                ))}\n              </CommandGroup>\n            )}\n          </CommandList>\n        </Command>\n      </PopoverContent>\n    </Popover>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/combobox/index.tsx","type":"registry:ui"}],"name":"combobox","registryDependencies":["command","popover","https://smoothui.dev/r/smooth-button.json","https://smoothui.dev/r/lib.json"],"title":"Combobox","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"An animated Context Menu component for SmoothUI with spring animations, nested submenus, and keyboard navigation.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport {\n  ContextMenuContent,\n  ContextMenuGroup,\n  ContextMenuItem,\n  ContextMenuLabel,\n  ContextMenu as ContextMenuRoot,\n  ContextMenuSeparator,\n  ContextMenuShortcut,\n  ContextMenuSub,\n  ContextMenuSubContent,\n  ContextMenuSubTrigger,\n  ContextMenuTrigger,\n} from \"@/components/ui/context-menu\";\nimport { cn } from \"@/lib/utils\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport type React from \"react\";\nimport { SPRING_DEFAULT } from \"@/components/smoothui/lib/animation\";\n\nexport interface ContextMenuProps {\n  /** The trigger element that opens the context menu on right-click */\n  children: React.ReactNode;\n  /** Optional CSS class for the content container */\n  className?: string;\n  /** Menu items to render */\n  items: ContextMenuItemConfig[];\n}\n\nexport interface ContextMenuItemConfig {\n  /** Nested submenu items */\n  children?: ContextMenuItemConfig[];\n  /** Whether the item is disabled */\n  disabled?: boolean;\n  /** Renders a group label instead of an item */\n  groupLabel?: string;\n  /** Optional icon to display before the label */\n  icon?: React.ReactNode;\n  /** Unique key for the item */\n  key: string;\n  /** Display label */\n  label: string;\n  /** Callback when item is selected */\n  onSelect?: () => void;\n  /** Renders a separator instead of an item */\n  separator?: boolean;\n  /** Optional keyboard shortcut text to display */\n  shortcut?: string;\n  /** Destructive variant styling */\n  variant?: \"default\" | \"destructive\";\n}\n\nexport default function ContextMenu({\n  children,\n  items,\n  className,\n}: ContextMenuProps) {\n  const shouldReduceMotion = useReducedMotion();\n\n  const renderItem = (item: ContextMenuItemConfig, index: number) => {\n    if (item.separator) {\n      return <ContextMenuSeparator key={item.key} />;\n    }\n\n    if (item.groupLabel) {\n      return (\n        <ContextMenuLabel key={item.key}>{item.groupLabel}</ContextMenuLabel>\n      );\n    }\n\n    if (item.children && item.children.length > 0) {\n      return (\n        <ContextMenuSub key={item.key}>\n          <ContextMenuSubTrigger disabled={item.disabled}>\n            {item.icon ? <span className=\"mr-2\">{item.icon}</span> : null}\n            {item.label}\n          </ContextMenuSubTrigger>\n          <ContextMenuSubContent>\n            {item.children.map((child, childIndex) =>\n              renderItem(child, childIndex)\n            )}\n          </ContextMenuSubContent>\n        </ContextMenuSub>\n      );\n    }\n\n    return (\n      <motion.div\n        animate={shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }}\n        initial={shouldReduceMotion ? { opacity: 1 } : { opacity: 0, y: -4 }}\n        key={item.key}\n        transition={\n          shouldReduceMotion\n            ? { duration: 0 }\n            : { ...SPRING_DEFAULT, delay: index * 0.02 }\n        }\n      >\n        <ContextMenuItem\n          disabled={item.disabled}\n          onSelect={item.onSelect}\n          variant={item.variant}\n        >\n          {item.icon ? <span className=\"mr-2\">{item.icon}</span> : null}\n          {item.label}\n          {item.shortcut ? (\n            <ContextMenuShortcut>{item.shortcut}</ContextMenuShortcut>\n          ) : null}\n        </ContextMenuItem>\n      </motion.div>\n    );\n  };\n\n  return (\n    <ContextMenuRoot>\n      <ContextMenuTrigger asChild>{children}</ContextMenuTrigger>\n      <ContextMenuContent className={cn(\"origin-top\", className)}>\n        <motion.div\n          animate={\n            shouldReduceMotion ? { opacity: 1 } : { opacity: 1, scale: 1, y: 0 }\n          }\n          initial={\n            shouldReduceMotion\n              ? { opacity: 1 }\n              : { opacity: 0, scale: 0.95, y: -4 }\n          }\n          transition={shouldReduceMotion ? { duration: 0 } : SPRING_DEFAULT}\n        >\n          <ContextMenuGroup>\n            {items.map((item, index) => renderItem(item, index))}\n          </ContextMenuGroup>\n        </motion.div>\n      </ContextMenuContent>\n    </ContextMenuRoot>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/context-menu/index.tsx","type":"registry:ui"}],"name":"context-menu","registryDependencies":["context-menu","https://smoothui.dev/r/lib.json"],"title":"Context Menu","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"A ContributionGraph component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { motion, useReducedMotion } from \"motion/react\";\nimport type React from \"react\";\nimport { useMemo, useState } from \"react\";\n\nexport interface ContributionData {\n  count: number;\n  date: string;\n  level: number;\n}\n\nexport interface ContributionGraphProps {\n  className?: string;\n  data?: ContributionData[];\n  showLegend?: boolean;\n  showTooltips?: boolean;\n  year?: number;\n}\n\nconst WEEKS_IN_YEAR = 53;\nconst DAYS_IN_WEEK = 7;\nconst JANUARY_MONTH = 0;\nconst DECEMBER_MONTH = 11;\nconst SUNDAY_DAY = 0;\nconst MIN_WEEKS_FOR_DECEMBER_HEADER = 2;\nconst TOOLTIP_OFFSET_X = 10;\nconst TOOLTIP_OFFSET_Y = 40;\n\nconst MONTHS = [\n  \"Jan\",\n  \"Feb\",\n  \"Mar\",\n  \"Apr\",\n  \"May\",\n  \"Jun\",\n  \"Jul\",\n  \"Aug\",\n  \"Sep\",\n  \"Oct\",\n  \"Nov\",\n  \"Dec\",\n];\n\nconst DAYS = [\"Sun\", \"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\"];\n\n// Contribution level colors (similar to GitHub's)\nconst CONTRIBUTION_COLORS = [\n  \"bg-primary\", // Level 0 - No contributions\n  \"bg-brand/25\", // Level 1\n  \"bg-brand/50\", // Level 2\n  \"bg-brand/75\", // Level 3\n  \"bg-brand\", // Level 4 - Max\n];\n\nconst LEVEL_0 = 0;\nconst LEVEL_1 = 1;\nconst LEVEL_2 = 2;\nconst LEVEL_3 = 3;\nconst LEVEL_4 = 4;\nconst CONTRIBUTION_LEVELS = [LEVEL_0, LEVEL_1, LEVEL_2, LEVEL_3, LEVEL_4];\nconst DAY_1 = 1;\nconst DAY_31 = 31;\n\n// Helper function to check if date is in valid range\nconst isDateInValidRange = (\n  currentDate: Date,\n  startDate: Date,\n  endDate: Date,\n  targetYear: number\n) => {\n  const isInRange = currentDate >= startDate && currentDate <= endDate;\n  const isPreviousYearDecember =\n    currentDate.getFullYear() === targetYear - 1 &&\n    currentDate.getMonth() === DECEMBER_MONTH;\n  const isNextYearJanuary =\n    currentDate.getFullYear() === targetYear + 1 &&\n    currentDate.getMonth() === JANUARY_MONTH;\n  return isInRange || isPreviousYearDecember || isNextYearJanuary;\n};\n\n// Helper function to create day data\nconst createDayData = (\n  currentDate: Date,\n  contributionData: ContributionData[]\n): ContributionData => {\n  const [dateString] = currentDate.toISOString().split(\"T\");\n  const existingData = contributionData.find((d) => d.date === dateString);\n  return {\n    count: existingData?.count ?? LEVEL_0,\n    date: dateString,\n    level: existingData?.level ?? LEVEL_0,\n  };\n};\n\n// Helper function to check if month should be shown\ninterface MonthHeaderCheck {\n  currentMonth: number;\n  currentYear: number;\n  startDateDay: number;\n  targetYear: number;\n  weekCount: number;\n}\nconst shouldShowMonthHeader = ({\n  currentYear,\n  targetYear,\n  currentMonth,\n  startDateDay,\n  weekCount,\n}: MonthHeaderCheck) =>\n  currentYear === targetYear ||\n  (currentYear === targetYear - 1 &&\n    currentMonth === DECEMBER_MONTH &&\n    startDateDay !== SUNDAY_DAY &&\n    weekCount >= MIN_WEEKS_FOR_DECEMBER_HEADER);\n\n// Helper function to calculate month headers\nconst calculateMonthHeaders = (targetYear: number) => {\n  const headers: { month: string; colspan: number; startWeek: number }[] = [];\n  const startDate = new Date(targetYear, JANUARY_MONTH, DAY_1);\n  const firstSunday = new Date(startDate);\n  firstSunday.setDate(startDate.getDate() - startDate.getDay());\n\n  let currentMonth = -1;\n  let currentYear = -1;\n  let monthStartWeek = 0;\n  let weekCount = 0;\n\n  for (let weekNumber = 0; weekNumber < WEEKS_IN_YEAR; weekNumber++) {\n    const weekDate = new Date(firstSunday);\n    weekDate.setDate(firstSunday.getDate() + weekNumber * DAYS_IN_WEEK);\n\n    const monthKey = weekDate.getMonth();\n    const yearKey = weekDate.getFullYear();\n\n    if (monthKey !== currentMonth || yearKey !== currentYear) {\n      if (\n        currentMonth !== -1 &&\n        shouldShowMonthHeader({\n          currentMonth,\n          currentYear,\n          startDateDay: startDate.getDay(),\n          targetYear,\n          weekCount,\n        })\n      ) {\n        headers.push({\n          colspan: weekCount,\n          month: MONTHS[currentMonth],\n          startWeek: monthStartWeek,\n        });\n      }\n      currentMonth = monthKey;\n      currentYear = yearKey;\n      monthStartWeek = weekNumber;\n      weekCount = 1;\n    } else {\n      weekCount++;\n    }\n  }\n\n  // Add the last month\n  if (\n    currentMonth !== -1 &&\n    shouldShowMonthHeader({\n      currentMonth,\n      currentYear,\n      startDateDay: startDate.getDay(),\n      targetYear,\n      weekCount,\n    })\n  ) {\n    headers.push({\n      colspan: weekCount,\n      month: MONTHS[currentMonth],\n      startWeek: monthStartWeek,\n    });\n  }\n\n  return headers;\n};\n\nexport function ContributionGraph({\n  data = [],\n  year = new Date().getFullYear(),\n  className = \"\",\n  showLegend = true,\n  showTooltips = true,\n}: ContributionGraphProps) {\n  const [hoveredDay, setHoveredDay] = useState<ContributionData | null>(null);\n  const [tooltipPosition, setTooltipPosition] = useState({ x: 0, y: 0 });\n  const shouldReduceMotion = useReducedMotion();\n\n  // Generate all days for the year\n  const yearData = useMemo(() => {\n    const startDate = new Date(year, JANUARY_MONTH, DAY_1);\n    const endDate = new Date(year, DECEMBER_MONTH, DAY_31);\n    const days: ContributionData[] = [];\n\n    // Start from the Sunday of the first week that contains January 1st\n    // This ensures December gets proper weeks before January\n    const firstSunday = new Date(startDate);\n    firstSunday.setDate(startDate.getDate() - startDate.getDay());\n\n    // Generate 53 weeks (GitHub shows 53 weeks)\n    for (let weekNum = 0; weekNum < WEEKS_IN_YEAR; weekNum++) {\n      for (let day = 0; day < DAYS_IN_WEEK; day++) {\n        const currentDate = new Date(firstSunday);\n        currentDate.setDate(\n          firstSunday.getDate() + weekNum * DAYS_IN_WEEK + day\n        );\n\n        if (isDateInValidRange(currentDate, startDate, endDate, year)) {\n          days.push(createDayData(currentDate, data));\n        } else {\n          // Add empty day for alignment\n          days.push({\n            count: LEVEL_0,\n            date: \"\",\n            level: LEVEL_0,\n          });\n        }\n      }\n    }\n\n    return days;\n  }, [data, year]);\n\n  // Calculate month headers with colspan\n  const monthHeaders = useMemo(() => calculateMonthHeaders(year), [year]);\n\n  const handleDayHover = (day: ContributionData, event: React.MouseEvent) => {\n    if (showTooltips && day.date) {\n      setHoveredDay(day);\n      setTooltipPosition({ x: event.clientX, y: event.clientY });\n    }\n  };\n\n  const handleDayLeave = () => {\n    setHoveredDay(null);\n  };\n\n  const formatDate = (dateString: string) => {\n    if (!dateString) {\n      return \"\";\n    }\n    const date = new Date(dateString);\n    return date.toLocaleDateString(\"en-US\", {\n      day: \"numeric\",\n      month: \"long\",\n      weekday: \"long\",\n      year: \"numeric\",\n    });\n  };\n\n  const getContributionText = (count: number) => {\n    if (count === LEVEL_0) {\n      return \"No contributions\";\n    }\n    if (count === LEVEL_1) {\n      return \"1 contribution\";\n    }\n    return `${count} contributions`;\n  };\n\n  return (\n    <div className={`contribution-graph ${className}`}>\n      <div className=\"overflow-x-auto\">\n        <table className=\"border-separate border-spacing-1 text-xs\">\n          <caption className=\"sr-only\">Contribution Graph for {year}</caption>\n\n          {/* Month Headers */}\n          <thead>\n            <tr className=\"h-3\">\n              <td className=\"w-7 min-w-7\" />\n              {monthHeaders.map((header) => (\n                <td\n                  className=\"relative text-left text-foreground\"\n                  colSpan={header.colspan}\n                  key={`${header.month}-${header.startWeek}`}\n                >\n                  <span className=\"absolute top-0 left-1\">{header.month}</span>\n                </td>\n              ))}\n            </tr>\n          </thead>\n\n          {/* Day Grid */}\n          <tbody>\n            {Array.from({ length: DAYS_IN_WEEK }, (_, dayIndex) => (\n              <tr className=\"h-2.5\" key={DAYS[dayIndex]}>\n                {/* Day Labels */}\n                <td className=\"relative w-7 min-w-7 text-foreground\">\n                  {dayIndex % 2 === 0 && (\n                    <span className=\"absolute -bottom-0.5 left-0 text-xs\">\n                      {DAYS[dayIndex]}\n                    </span>\n                  )}\n                </td>\n\n                {/* Day Cells */}\n                {Array.from({ length: WEEKS_IN_YEAR }, (_week, weekIndex) => {\n                  const dayData = yearData[weekIndex * DAYS_IN_WEEK + dayIndex];\n                  const cellKey = `${dayData?.date ?? \"empty\"}-${weekIndex}-${dayIndex}`;\n                  if (!dayData?.date) {\n                    return (\n                      <td className=\"h-2.5 w-2.5 p-0\" key={cellKey}>\n                        <div className=\"h-2.5 w-2.5\" />\n                      </td>\n                    );\n                  }\n\n                  return (\n                    // biome-ignore lint/a11y/noNoninteractiveElementInteractions: Table cell is interactive for hover tooltips\n                    <td\n                      className=\"h-2.5 w-2.5 cursor-pointer p-0\"\n                      key={cellKey}\n                      onMouseEnter={(e) => handleDayHover(dayData, e)}\n                      onMouseLeave={handleDayLeave}\n                      title={\n                        showTooltips\n                          ? `${formatDate(dayData.date)}: ${getContributionText(dayData.count)}`\n                          : undefined\n                      }\n                    >\n                      <div\n                        className={`h-2.5 w-2.5 rounded-sm ${\n                          CONTRIBUTION_COLORS[dayData.level]\n                        } hover:ring-2 hover:ring-background`}\n                      />\n                    </td>\n                  );\n                })}\n              </tr>\n            ))}\n          </tbody>\n        </table>\n      </div>\n\n      {/* Tooltip */}\n      {showTooltips && hoveredDay ? (\n        <motion.div\n          animate={\n            shouldReduceMotion ? { opacity: 1 } : { opacity: 1, scale: 1 }\n          }\n          className=\"pointer-events-none fixed z-50 rounded-lg border bg-primary px-3 py-2 text-foreground text-sm shadow-lg\"\n          exit={\n            shouldReduceMotion\n              ? { opacity: 0, transition: { duration: 0 } }\n              : { opacity: 0, scale: 0.8 }\n          }\n          initial={\n            shouldReduceMotion ? { opacity: 1 } : { opacity: 0, scale: 0.8 }\n          }\n          style={{\n            left: tooltipPosition.x + TOOLTIP_OFFSET_X,\n            top: tooltipPosition.y - TOOLTIP_OFFSET_Y,\n          }}\n          transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.2 }}\n        >\n          <div className=\"font-semibold\">\n            {getContributionText(hoveredDay.count)}\n          </div>\n          <div className=\"text-foreground/70\">\n            {formatDate(hoveredDay.date)}\n          </div>\n        </motion.div>\n      ) : null}\n\n      {/* Legend */}\n      {showLegend ? (\n        <div className=\"mt-4 flex items-center justify-between text-foreground/70 text-xs\">\n          <span>Less</span>\n          <div className=\"flex items-center gap-1\">\n            {CONTRIBUTION_LEVELS.map((level) => (\n              <div\n                className={`h-3 w-3 rounded-sm ${CONTRIBUTION_COLORS[level]}`}\n                key={level}\n              />\n            ))}\n          </div>\n          <span>More</span>\n        </div>\n      ) : null}\n    </div>\n  );\n}\n\nexport default ContributionGraph;\n","path":"index.tsx","target":"components/smoothui/contribution-graph/index.tsx","type":"registry:ui"}],"name":"contribution-graph","registryDependencies":["https://smoothui.dev/r/tokens.json"],"title":"Contribution Graph","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"A CursorFollow component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport {\n  motion,\n  useMotionValue,\n  useReducedMotion,\n  useSpring,\n} from \"motion/react\";\nimport type React from \"react\";\nimport { useEffect, useRef, useState } from \"react\";\n\nimport { useCursorPosition } from \"./use-cursor-position\";\n\nexport interface CursorFollowProps {\n  children: React.ReactNode;\n  className?: string;\n}\n\nconst CIRCLE_SIZE = 16;\nconst MIN_BUBBLE_WIDTH = 40;\nconst BUBBLE_HEIGHT = 40;\nconst TEXT_PADDING = 32;\n\nconst CursorFollow: React.FC<CursorFollowProps> = ({\n  children,\n  className = \"\",\n}) => {\n  const { x: mouseX, y: mouseY } = useCursorPosition();\n  const [cursorText, setCursorText] = useState<string | null>(null);\n  const [pendingText, setPendingText] = useState<string | null>(null);\n  const [textWidth, setTextWidth] = useState<number>(0);\n  const measureRef = useRef<HTMLSpanElement>(null);\n  const shouldReduceMotion = useReducedMotion();\n\n  // Motion values for smooth follow\n  const x = useMotionValue(0);\n  const y = useMotionValue(0);\n  const springX = useSpring(x, { damping: 40, stiffness: 350 });\n  const springY = useSpring(y, { damping: 40, stiffness: 350 });\n\n  // Calculate bubble width and height\n  const bubbleWidth = cursorText\n    ? Math.max(textWidth + TEXT_PADDING, MIN_BUBBLE_WIDTH)\n    : CIRCLE_SIZE;\n  const bubbleHeight = cursorText ? BUBBLE_HEIGHT : CIRCLE_SIZE;\n\n  // Update target position on mouse move\n  useEffect(() => {\n    x.set(mouseX - bubbleWidth / 2);\n    y.set(mouseY - bubbleHeight / 2);\n  }, [mouseX, mouseY, bubbleWidth, bubbleHeight, x, y]);\n\n  // Pre-measure text width before showing bubble\n  useEffect(() => {\n    if (pendingText && measureRef.current) {\n      const width = measureRef.current.offsetWidth;\n      setTextWidth(width);\n      setCursorText(pendingText);\n      setPendingText(null);\n    }\n    if (!(pendingText || cursorText)) {\n      setTextWidth(0);\n    }\n  }, [pendingText, cursorText]);\n\n  // Handlers for child hover\n  const handleMouseOver = (e: React.MouseEvent) => {\n    const target = e.target as HTMLElement;\n    const text = target.getAttribute(\"data-cursor-text\");\n    if (text) {\n      setPendingText(text);\n    }\n  };\n  const handleMouseOut = () => {\n    setCursorText(null);\n    setPendingText(null);\n  };\n  const handleFocus = (e: React.FocusEvent) => {\n    const target = e.target as HTMLElement;\n    const text = target.getAttribute(\"data-cursor-text\");\n    if (text) {\n      setPendingText(text);\n    }\n  };\n  const handleBlur = () => {\n    setCursorText(null);\n    setPendingText(null);\n  };\n\n  return (\n    // biome-ignore lint/a11y/noNoninteractiveElementInteractions: Interactive cursor tracking widget requires mouse events\n    <div\n      className={`relative h-full w-full ${className}`}\n      onBlur={handleBlur}\n      onFocus={handleFocus}\n      onMouseOut={handleMouseOut}\n      onMouseOver={handleMouseOver}\n      role=\"application\"\n      style={{ cursor: \"none\", minHeight: 300 }}\n      // biome-ignore lint/a11y/noNoninteractiveTabindex: Interactive cursor tracking widget requires focus\n      tabIndex={0}\n    >\n      {children}\n      <motion.div\n        animate={\n          shouldReduceMotion\n            ? { opacity: 1, scale: 1 }\n            : {\n                opacity: 1,\n                scale: 1,\n                transition: {\n                  duration: 0.25,\n                  ease: [0.645, 0.045, 0.355, 1],\n                },\n              }\n        }\n        className=\"pointer-events-none fixed z-50\"\n        exit={shouldReduceMotion ? {} : { opacity: 0, scale: 0.7 }}\n        initial={\n          shouldReduceMotion\n            ? { opacity: 1, scale: 1 }\n            : { opacity: 0, scale: 0.7 }\n        }\n        style={{ left: 0, top: 0, x: springX, y: springY }}\n      >\n        <motion.div\n          animate={\n            cursorText\n              ? {\n                  background: \"var(--color-brand, #6366f1)\",\n                  borderRadius: 20,\n                  color: \"#fff\",\n                  height: 40,\n                  minHeight: 32,\n                  minWidth: 40,\n                  paddingLeft: 16,\n                  paddingRight: 16,\n                  scale: 1.1,\n                  width: bubbleWidth,\n                }\n              : {\n                  background: \"var(--color-brand, #6366f1)\",\n                  borderRadius: 999,\n                  color: \"#fff\",\n                  height: CIRCLE_SIZE,\n                  minHeight: CIRCLE_SIZE,\n                  minWidth: CIRCLE_SIZE,\n                  paddingLeft: 0,\n                  paddingRight: 0,\n                  scale: 1,\n                  width: CIRCLE_SIZE,\n                }\n          }\n          className=\"flex items-center justify-center font-medium text-xs shadow-lg\"\n          layout\n          style={{\n            alignItems: \"center\",\n            boxShadow: \"0 2px 8px 0 rgba(0,0,0,0.10)\",\n            display: \"flex\",\n            justifyContent: \"center\",\n            position: \"relative\",\n            zIndex: 1,\n          }}\n          transition={\n            shouldReduceMotion\n              ? { duration: 0 }\n              : { duration: 0.25, ease: [0.645, 0.045, 0.355, 1] }\n          }\n        >\n          {cursorText ? (\n            <motion.span\n              animate={{ filter: \"blur(0px)\", opacity: 1 }}\n              exit={{ filter: \"blur(8px)\", opacity: 0 }}\n              initial={{ filter: \"blur(8px)\", opacity: 0 }}\n              style={{\n                color: \"#fff\",\n                textAlign: \"center\",\n                whiteSpace: \"nowrap\",\n                width: \"100%\",\n              }}\n              transition={\n                shouldReduceMotion\n                  ? { duration: 0 }\n                  : {\n                      delay: 0.05,\n                      duration: 0.2,\n                      ease: [0.645, 0.045, 0.355, 1],\n                    }\n              }\n            >\n              {cursorText}\n            </motion.span>\n          ) : null}\n        </motion.div>\n        {/* Hidden span for pre-measuring text width */}\n        {pendingText || cursorText ? (\n          <span\n            ref={measureRef}\n            style={{\n              fontFamily: \"inherit\",\n              fontSize: \"0.75rem\",\n              fontWeight: 500,\n              paddingLeft: 16,\n              paddingRight: 16,\n              pointerEvents: \"none\",\n              position: \"absolute\",\n              visibility: \"hidden\",\n              whiteSpace: \"nowrap\",\n            }}\n          >\n            {pendingText || cursorText}\n          </span>\n        ) : null}\n      </motion.div>\n    </div>\n  );\n};\n\nexport default CursorFollow;\n","path":"index.tsx","target":"components/smoothui/cursor-follow/index.tsx","type":"registry:ui"},{"content":"import { useEffect, useState } from \"react\";\n\nexport function useCursorPosition() {\n  const [position, setPosition] = useState({ x: 0, y: 0 });\n\n  useEffect(() => {\n    const handleMouseMove = (event: MouseEvent) => {\n      setPosition({ x: event.clientX, y: event.clientY });\n    };\n    window.addEventListener(\"mousemove\", handleMouseMove);\n    return () => window.removeEventListener(\"mousemove\", handleMouseMove);\n  }, []);\n\n  return position;\n}\n","path":"use-cursor-position.tsx","target":"components/smoothui/cursor-follow/use-cursor-position.tsx","type":"registry:ui"}],"name":"cursor-follow","registryDependencies":["https://smoothui.dev/r/tokens.json"],"title":"Cursor Follow","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"A DepthParallaxWords text animation component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { motion, useInView, useReducedMotion } from \"motion/react\";\nimport { useRef } from \"react\";\n\n/**\n * DepthParallaxWords — per-word depth motion with scale, vertical drift and blur for layered readability.\n * From the animate-text catalog (`depth-parallax-words`).\n */\nexport interface DepthParallaxWordsProps {\n  children: string;\n  className?: string;\n  /** Delay before the animation starts, in milliseconds. */\n  delay?: number;\n  /** Per-word stagger, in milliseconds. */\n  stagger?: number;\n  /** Animate only once the text scrolls into view. */\n  triggerOnView?: boolean;\n}\n\nconst DURATION_S = 0.7;\nconst MS = 1000;\nconst EASE = [0.22, 1, 0.36, 1] as const;\n\nexport default function DepthParallaxWords({\n  children,\n  className = \"\",\n  delay = 0,\n  stagger = 70,\n  triggerOnView = false,\n}: DepthParallaxWordsProps) {\n  const ref = useRef<HTMLSpanElement>(null);\n  const inView = useInView(ref, { once: true });\n  const shouldReduceMotion = useReducedMotion();\n  const play = (!triggerOnView || inView) && !shouldReduceMotion;\n  const words = children.split(\" \");\n\n  return (\n    <span aria-label={children} className={className} ref={ref}>\n      {words.map((word, index) => (\n        // biome-ignore lint/suspicious/noArrayIndexKey: words have no stable id\n        <span key={index}>\n          <motion.span\n            animate={\n              play\n                ? { filter: \"blur(0px)\", opacity: 1, scale: 1, y: 0 }\n                : undefined\n            }\n            aria-hidden=\"true\"\n            initial={\n              shouldReduceMotion\n                ? { opacity: 1 }\n                : { filter: \"blur(3px)\", opacity: 0, scale: 0.92, y: 18 }\n            }\n            style={{ display: \"inline-block\", whiteSpace: \"pre\" }}\n            transition={\n              shouldReduceMotion\n                ? { duration: 0 }\n                : {\n                    delay: delay / MS + (index * stagger) / MS,\n                    duration: DURATION_S,\n                    ease: EASE,\n                  }\n            }\n          >\n            {word}\n          </motion.span>\n          {index < words.length - 1 && (\n            <span\n              aria-hidden=\"true\"\n              style={{ display: \"inline-block\", whiteSpace: \"pre\" }}\n            >\n              {\" \"}\n            </span>\n          )}\n        </span>\n      ))}\n    </span>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/depth-parallax-words/index.tsx","type":"registry:ui"}],"name":"depth-parallax-words","registryDependencies":[],"title":"Depth Parallax Words","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["lucide-react","motion","radix-ui"],"description":"Animated Dialog and AlertDialog components for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport {\n  AlertDialogAction,\n  AlertDialogCancel,\n  AlertDialogDescription,\n  AlertDialogFooter,\n  AlertDialogHeader,\n  AlertDialogTitle,\n  AlertDialogTrigger,\n} from \"@/components/ui/alert-dialog\";\nimport {\n  DialogClose,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from \"@/components/ui/dialog\";\nimport { cn } from \"@/lib/utils\";\nimport { X } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport {\n  AlertDialog as AlertDialogRadix,\n  Dialog as DialogRadix,\n} from \"radix-ui\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\n\n/* ------------------------------------------------------------------ */\n/*  Shared animation constants                                        */\n/* ------------------------------------------------------------------ */\n\nconst SPRING_PANEL = {\n  bounce: 0.05,\n  duration: 0.25,\n  type: \"spring\" as const,\n};\n\nconst BACKDROP_DURATION = 0.2;\n\nconst STAGGER_DELAY = 0.06;\n\n/* ------------------------------------------------------------------ */\n/*  Types                                                              */\n/* ------------------------------------------------------------------ */\n\nexport interface DialogProps {\n  /** Dialog content */\n  children?: React.ReactNode;\n  /** Additional CSS class names for the content */\n  className?: string;\n  /** Description displayed below the title */\n  description?: string;\n  /** Footer content */\n  footer?: React.ReactNode;\n  /** Callback when the open state changes */\n  onOpenChange?: (open: boolean) => void;\n  /** Whether the dialog is open */\n  open?: boolean;\n  /** Whether to show the close button */\n  showCloseButton?: boolean;\n  /** Title displayed in the dialog header */\n  title?: string;\n  /** Trigger element that opens the dialog */\n  trigger?: React.ReactNode;\n}\n\nexport interface AlertDialogProps {\n  /** Alert dialog content */\n  children?: React.ReactNode;\n  /** Additional CSS class names for the content */\n  className?: string;\n  /** Description displayed below the title */\n  description?: string;\n  /** Footer content (typically AlertDialogAction + AlertDialogCancel) */\n  footer?: React.ReactNode;\n  /** Callback when the open state changes */\n  onOpenChange?: (open: boolean) => void;\n  /** Whether the alert dialog is open */\n  open?: boolean;\n  /** Title displayed in the alert dialog header */\n  title?: string;\n  /** Trigger element that opens the alert dialog */\n  trigger?: React.ReactNode;\n}\n\n/* ------------------------------------------------------------------ */\n/*  Stagger wrapper — animates children in sequence                   */\n/* ------------------------------------------------------------------ */\n\nconst StaggerChild = ({\n  children,\n  index,\n  shouldReduceMotion,\n}: {\n  children: React.ReactNode;\n  index: number;\n  shouldReduceMotion: boolean | null;\n}) => (\n  <motion.div\n    animate={\n      shouldReduceMotion\n        ? { opacity: 1 }\n        : { opacity: 1, transform: \"translateY(0px)\" }\n    }\n    exit={\n      shouldReduceMotion\n        ? { opacity: 0, transition: { duration: 0 } }\n        : {\n            opacity: 0,\n            transform: \"translateY(4px)\",\n            transition: { duration: 0.12 },\n          }\n    }\n    initial={\n      shouldReduceMotion\n        ? { opacity: 1 }\n        : { opacity: 0, transform: \"translateY(8px)\" }\n    }\n    transition={\n      shouldReduceMotion\n        ? { duration: 0 }\n        : {\n            bounce: 0,\n            delay: index * STAGGER_DELAY,\n            duration: 0.25,\n            type: \"spring\" as const,\n          }\n    }\n  >\n    {children}\n  </motion.div>\n);\n\n/* ------------------------------------------------------------------ */\n/*  Animated close button with rotate on hover                        */\n/* ------------------------------------------------------------------ */\n\nconst AnimatedCloseButton = ({\n  onClick,\n  shouldReduceMotion,\n}: {\n  onClick: () => void;\n  shouldReduceMotion: boolean | null;\n}) => (\n  <motion.button\n    aria-label=\"Close dialog\"\n    className=\"absolute top-4 right-4 cursor-pointer rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none\"\n    onClick={onClick}\n    transition={{ duration: shouldReduceMotion ? 0 : 0.2 }}\n    type=\"button\"\n    whileHover={shouldReduceMotion ? {} : { rotate: 90 }}\n  >\n    <X aria-hidden=\"true\" className=\"pointer-events-none size-4 shrink-0\" />\n    <span className=\"sr-only\">Close</span>\n  </motion.button>\n);\n\n/* ------------------------------------------------------------------ */\n/*  Dialog                                                             */\n/* ------------------------------------------------------------------ */\n\nexport default function Dialog({\n  open,\n  onOpenChange,\n  title,\n  description,\n  showCloseButton = true,\n  className,\n  children,\n  trigger,\n  footer,\n}: DialogProps) {\n  const shouldReduceMotion = useReducedMotion();\n  const [internalOpen, setInternalOpen] = useState(false);\n  const isControlled = open !== undefined;\n  const isOpen = isControlled ? open : internalOpen;\n\n  const handleOpenChange = useCallback(\n    (next: boolean) => {\n      if (!isControlled) {\n        setInternalOpen(next);\n      }\n      onOpenChange?.(next);\n    },\n    [isControlled, onOpenChange]\n  );\n\n  // Track mount state for AnimatePresence exit animations\n  const [showContent, setShowContent] = useState(false);\n  const prevOpenRef = useRef(false);\n\n  useEffect(() => {\n    if (isOpen && !prevOpenRef.current) {\n      setShowContent(true);\n    }\n    prevOpenRef.current = !!isOpen;\n  }, [isOpen]);\n\n  const handleAnimationComplete = useCallback(() => {\n    if (!isOpen) {\n      setShowContent(false);\n    }\n  }, [isOpen]);\n\n  return (\n    <DialogRadix.Root\n      onOpenChange={handleOpenChange}\n      open={isOpen || showContent}\n    >\n      {trigger ? <DialogTrigger asChild>{trigger}</DialogTrigger> : null}\n\n      <AnimatePresence onExitComplete={() => setShowContent(false)}>\n        {isOpen ? (\n          <DialogRadix.Portal forceMount>\n            {/* Backdrop with blur + opacity fade */}\n            <DialogRadix.Overlay asChild forceMount>\n              <motion.div\n                animate={{ opacity: 1 }}\n                className=\"fixed inset-0 z-50 bg-black/50 backdrop-blur-sm\"\n                data-slot=\"dialog-overlay\"\n                exit={{ opacity: 0 }}\n                initial={shouldReduceMotion ? { opacity: 1 } : { opacity: 0 }}\n                transition={{\n                  duration: shouldReduceMotion ? 0 : BACKDROP_DURATION,\n                }}\n              />\n            </DialogRadix.Overlay>\n\n            {/* Content panel — spring scale + translateY entrance */}\n            <DialogRadix.Content asChild forceMount>\n              <motion.div\n                animate={\n                  shouldReduceMotion\n                    ? { opacity: 1 }\n                    : {\n                        opacity: 1,\n                        transform: \"translate(-50%, -50%) scale(1)\",\n                      }\n                }\n                className={cn(\n                  \"fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] gap-4 rounded-lg border bg-background p-6 shadow-lg sm:max-w-lg\",\n                  className\n                )}\n                data-slot=\"dialog-content\"\n                exit={\n                  shouldReduceMotion\n                    ? { opacity: 0, transition: { duration: 0 } }\n                    : {\n                        opacity: 0,\n                        transform: \"translate(-50%, -50%) scale(0.95)\",\n                        transition: { duration: 0.15 },\n                      }\n                }\n                initial={\n                  shouldReduceMotion\n                    ? {\n                        opacity: 1,\n                        transform: \"translate(-50%, -50%) scale(1)\",\n                      }\n                    : {\n                        opacity: 0,\n                        transform: \"translate(-50%, -48%) scale(0.95)\",\n                      }\n                }\n                onAnimationComplete={handleAnimationComplete}\n                transition={shouldReduceMotion ? { duration: 0 } : SPRING_PANEL}\n              >\n                {/* Staggered content sections */}\n                {title || description ? (\n                  <StaggerChild\n                    index={0}\n                    shouldReduceMotion={shouldReduceMotion}\n                  >\n                    <DialogHeader>\n                      {title ? <DialogTitle>{title}</DialogTitle> : null}\n                      {description ? (\n                        <DialogDescription>{description}</DialogDescription>\n                      ) : null}\n                    </DialogHeader>\n                  </StaggerChild>\n                ) : null}\n\n                {children ? (\n                  <StaggerChild\n                    index={title || description ? 1 : 0}\n                    shouldReduceMotion={shouldReduceMotion}\n                  >\n                    {children}\n                  </StaggerChild>\n                ) : null}\n\n                {footer ? (\n                  <StaggerChild\n                    index={(title || description ? 1 : 0) + (children ? 1 : 0)}\n                    shouldReduceMotion={shouldReduceMotion}\n                  >\n                    <DialogFooter>{footer}</DialogFooter>\n                  </StaggerChild>\n                ) : null}\n\n                {showCloseButton ? (\n                  <AnimatedCloseButton\n                    onClick={() => handleOpenChange(false)}\n                    shouldReduceMotion={shouldReduceMotion}\n                  />\n                ) : null}\n              </motion.div>\n            </DialogRadix.Content>\n          </DialogRadix.Portal>\n        ) : null}\n      </AnimatePresence>\n    </DialogRadix.Root>\n  );\n}\n\n/* ------------------------------------------------------------------ */\n/*  AlertDialog                                                        */\n/* ------------------------------------------------------------------ */\n\nexport function AlertDialog({\n  open,\n  onOpenChange,\n  title,\n  description,\n  className,\n  children,\n  trigger,\n  footer,\n}: AlertDialogProps) {\n  const shouldReduceMotion = useReducedMotion();\n  const [internalOpen, setInternalOpen] = useState(false);\n  const isControlled = open !== undefined;\n  const isOpen = isControlled ? open : internalOpen;\n\n  const handleOpenChange = useCallback(\n    (next: boolean) => {\n      if (!isControlled) {\n        setInternalOpen(next);\n      }\n      onOpenChange?.(next);\n    },\n    [isControlled, onOpenChange]\n  );\n\n  const [showContent, setShowContent] = useState(false);\n  const prevOpenRef = useRef(false);\n\n  useEffect(() => {\n    if (isOpen && !prevOpenRef.current) {\n      setShowContent(true);\n    }\n    prevOpenRef.current = !!isOpen;\n  }, [isOpen]);\n\n  const handleAnimationComplete = useCallback(() => {\n    if (!isOpen) {\n      setShowContent(false);\n    }\n  }, [isOpen]);\n\n  return (\n    <AlertDialogRadix.Root\n      onOpenChange={handleOpenChange}\n      open={isOpen || showContent}\n    >\n      {trigger ? (\n        <AlertDialogTrigger asChild>{trigger}</AlertDialogTrigger>\n      ) : null}\n\n      <AnimatePresence onExitComplete={() => setShowContent(false)}>\n        {isOpen ? (\n          <AlertDialogRadix.Portal forceMount>\n            {/* Backdrop */}\n            <AlertDialogRadix.Overlay asChild forceMount>\n              <motion.div\n                animate={{ opacity: 1 }}\n                className=\"fixed inset-0 z-50 bg-black/50 backdrop-blur-sm\"\n                data-slot=\"alert-dialog-overlay\"\n                exit={{ opacity: 0 }}\n                initial={shouldReduceMotion ? { opacity: 1 } : { opacity: 0 }}\n                transition={{\n                  duration: shouldReduceMotion ? 0 : BACKDROP_DURATION,\n                }}\n              />\n            </AlertDialogRadix.Overlay>\n\n            {/* Content panel */}\n            <AlertDialogRadix.Content asChild forceMount>\n              <motion.div\n                animate={\n                  shouldReduceMotion\n                    ? { opacity: 1 }\n                    : {\n                        opacity: 1,\n                        transform: \"translate(-50%, -50%) scale(1)\",\n                      }\n                }\n                className={cn(\n                  \"fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] gap-4 rounded-lg border bg-background p-6 shadow-lg sm:max-w-lg\",\n                  className\n                )}\n                data-slot=\"alert-dialog-content\"\n                exit={\n                  shouldReduceMotion\n                    ? { opacity: 0, transition: { duration: 0 } }\n                    : {\n                        opacity: 0,\n                        transform: \"translate(-50%, -50%) scale(0.95)\",\n                        transition: { duration: 0.15 },\n                      }\n                }\n                initial={\n                  shouldReduceMotion\n                    ? {\n                        opacity: 1,\n                        transform: \"translate(-50%, -50%) scale(1)\",\n                      }\n                    : {\n                        opacity: 0,\n                        transform: \"translate(-50%, -48%) scale(0.95)\",\n                      }\n                }\n                onAnimationComplete={handleAnimationComplete}\n                transition={shouldReduceMotion ? { duration: 0 } : SPRING_PANEL}\n              >\n                {title || description ? (\n                  <StaggerChild\n                    index={0}\n                    shouldReduceMotion={shouldReduceMotion}\n                  >\n                    <AlertDialogHeader>\n                      {title ? (\n                        <AlertDialogTitle>{title}</AlertDialogTitle>\n                      ) : null}\n                      {description ? (\n                        <AlertDialogDescription>\n                          {description}\n                        </AlertDialogDescription>\n                      ) : null}\n                    </AlertDialogHeader>\n                  </StaggerChild>\n                ) : null}\n\n                {children ? (\n                  <StaggerChild\n                    index={title || description ? 1 : 0}\n                    shouldReduceMotion={shouldReduceMotion}\n                  >\n                    {children}\n                  </StaggerChild>\n                ) : null}\n\n                {footer ? (\n                  <StaggerChild\n                    index={(title || description ? 1 : 0) + (children ? 1 : 0)}\n                    shouldReduceMotion={shouldReduceMotion}\n                  >\n                    <AlertDialogFooter>{footer}</AlertDialogFooter>\n                  </StaggerChild>\n                ) : null}\n              </motion.div>\n            </AlertDialogRadix.Content>\n          </AlertDialogRadix.Portal>\n        ) : null}\n      </AnimatePresence>\n    </AlertDialogRadix.Root>\n  );\n}\n\n/* ------------------------------------------------------------------ */\n/*  Re-exports                                                         */\n/* ------------------------------------------------------------------ */\n\nexport {\n  AlertDialogAction,\n  AlertDialogCancel,\n  AlertDialogDescription,\n  AlertDialogFooter,\n  AlertDialogHeader,\n  AlertDialogTitle,\n  AlertDialogTrigger,\n  DialogClose,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n};\n","path":"index.tsx","target":"components/smoothui/dialog/index.tsx","type":"registry:ui"}],"name":"dialog","registryDependencies":["alert-dialog","dialog"],"title":"Dialog","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"A DotMorphButton component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport SmoothButton from \"@/components/smoothui/smooth-button\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { useEffect, useState } from \"react\";\n\nexport interface DotMorphButtonProps {\n  className?: string;\n  label: string;\n  onClick?: () => void;\n}\n\nexport function DotMorphButton({\n  label,\n  className = \"\",\n  onClick,\n}: DotMorphButtonProps) {\n  const [isHovered, setIsHovered] = useState(false);\n  const shouldReduceMotion = useReducedMotion();\n  const [isHoverDevice, setIsHoverDevice] = useState(false);\n\n  useEffect(() => {\n    const mediaQuery = window.matchMedia(\"(hover: hover) and (pointer: fine)\");\n    setIsHoverDevice(mediaQuery.matches);\n\n    const handleChange = (e: MediaQueryListEvent) => {\n      setIsHoverDevice(e.matches);\n    };\n\n    mediaQuery.addEventListener(\"change\", handleChange);\n    return () => mediaQuery.removeEventListener(\"change\", handleChange);\n  }, []);\n\n  return (\n    <SmoothButton\n      className={`flex items-center gap-3 rounded-full border bg-background ${className}`}\n      onClick={onClick}\n      onMouseEnter={() => {\n        if (isHoverDevice) {\n          setIsHovered(true);\n        }\n      }}\n      onMouseLeave={() => setIsHovered(false)}\n      type=\"button\"\n    >\n      <motion.span\n        animate={\n          shouldReduceMotion || !isHoverDevice || !isHovered\n            ? {\n                borderRadius: 999,\n                height: 16,\n                width: 16,\n              }\n            : {\n                borderRadius: 999,\n                height: 28,\n                width: 12,\n              }\n        }\n        className=\"flex items-center justify-center\"\n        initial={false}\n        style={{\n          background: \"var(--color-brand)\",\n          display: \"inline-block\",\n        }}\n        transition={\n          shouldReduceMotion\n            ? { duration: 0 }\n            : {\n                damping: 22,\n                duration: 0.25,\n                stiffness: 600,\n                type: \"spring\" as const,\n              }\n        }\n      />\n      <span className=\"select-none font-medium text-2xl text-foreground\">\n        {label}\n      </span>\n    </SmoothButton>\n  );\n}\n\nexport default DotMorphButton;\n","path":"index.tsx","target":"components/smoothui/dot-morph-button/index.tsx","type":"registry:ui"}],"name":"dot-morph-button","registryDependencies":["https://smoothui.dev/r/smooth-button.json","https://smoothui.dev/r/tokens.json"],"title":"Dot Morph Button","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"An animated Drawer component for SmoothUI that slides from any side.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport {\n  DrawerClose,\n  DrawerContent,\n  DrawerDescription,\n  DrawerFooter,\n  DrawerHeader,\n  Drawer as DrawerPrimitive,\n  DrawerTitle,\n  DrawerTrigger,\n} from \"@/components/ui/drawer\";\nimport { cn } from \"@/lib/utils\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\n\n/* ------------------------------------------------------------------ */\n/*  Animation constants                                                */\n/* ------------------------------------------------------------------ */\n\nconst BACKDROP_DURATION = 0.2;\nconst STAGGER_BASE_DELAY = 0.12;\nconst STAGGER_CHILD_DELAY = 0.05;\n\n/* ------------------------------------------------------------------ */\n/*  Types                                                              */\n/* ------------------------------------------------------------------ */\n\nexport type DrawerSide = \"top\" | \"right\" | \"bottom\" | \"left\";\n\nexport interface DrawerProps {\n  /** Drawer content */\n  children?: React.ReactNode;\n  /** Additional CSS class names */\n  className?: string;\n  /** Description displayed below the title */\n  description?: string;\n  /** Footer content */\n  footer?: React.ReactNode;\n  /** Callback when the open state changes */\n  onOpenChange?: (open: boolean) => void;\n  /** Whether the drawer is open */\n  open?: boolean;\n  /** The side from which the drawer opens */\n  side?: DrawerSide;\n  /** Title displayed in the drawer header */\n  title?: string;\n  /** Trigger element that opens the drawer */\n  trigger?: React.ReactNode;\n}\n\n/* ------------------------------------------------------------------ */\n/*  Stagger child — animates content items in sequence after open      */\n/* ------------------------------------------------------------------ */\n\nconst StaggerChild = ({\n  children,\n  index,\n  shouldReduceMotion,\n}: {\n  children: React.ReactNode;\n  index: number;\n  shouldReduceMotion: boolean | null;\n}) => (\n  <motion.div\n    animate={\n      shouldReduceMotion\n        ? { opacity: 1 }\n        : { opacity: 1, transform: \"translateY(0px)\" }\n    }\n    exit={\n      shouldReduceMotion\n        ? { opacity: 0, transition: { duration: 0 } }\n        : { opacity: 0, transition: { duration: 0.1 } }\n    }\n    initial={\n      shouldReduceMotion\n        ? { opacity: 1 }\n        : { opacity: 0, transform: \"translateY(6px)\" }\n    }\n    transition={\n      shouldReduceMotion\n        ? { duration: 0 }\n        : {\n            bounce: 0,\n            delay: STAGGER_BASE_DELAY + index * STAGGER_CHILD_DELAY,\n            duration: 0.25,\n            type: \"spring\" as const,\n          }\n    }\n  >\n    {children}\n  </motion.div>\n);\n\n/* ------------------------------------------------------------------ */\n/*  Animated handle bar with glow pulse                                */\n/* ------------------------------------------------------------------ */\n\nconst AnimatedHandle = ({\n  shouldReduceMotion,\n}: {\n  shouldReduceMotion: boolean | null;\n}) => (\n  <motion.div\n    animate={\n      shouldReduceMotion\n        ? {}\n        : {\n            opacity: [0.5, 1, 0.5],\n          }\n    }\n    className=\"mx-auto mt-4 h-2 w-[100px] shrink-0 rounded-full bg-muted\"\n    transition={\n      shouldReduceMotion\n        ? { duration: 0 }\n        : {\n            duration: 2,\n            ease: [0.645, 0.045, 0.355, 1],\n            repeat: Number.POSITIVE_INFINITY,\n          }\n    }\n  />\n);\n\n/* ------------------------------------------------------------------ */\n/*  Drawer                                                             */\n/* ------------------------------------------------------------------ */\n\nexport default function Drawer({\n  open,\n  onOpenChange,\n  side = \"bottom\",\n  title,\n  description,\n  className,\n  children,\n  trigger,\n  footer,\n}: DrawerProps) {\n  const shouldReduceMotion = useReducedMotion();\n  const [isAnimatingOut, setIsAnimatingOut] = useState(false);\n  const prevOpenRef = useRef(false);\n\n  useEffect(() => {\n    // When going from open -> closed, we need animation-out time\n    if (prevOpenRef.current && !open) {\n      setIsAnimatingOut(true);\n    }\n    prevOpenRef.current = !!open;\n  }, [open]);\n\n  const handleExitComplete = useCallback(() => {\n    setIsAnimatingOut(false);\n  }, []);\n\n  // Keep vaul's drawer open during exit animation so the portal stays mounted\n  const vaulOpen = open || isAnimatingOut;\n\n  return (\n    <DrawerPrimitive\n      direction={side}\n      onOpenChange={(next) => {\n        if (!next && isAnimatingOut) {\n          return; // Ignore vaul's close during our exit animation\n        }\n        onOpenChange?.(next);\n      }}\n      open={vaulOpen}\n    >\n      {trigger ? <DrawerTrigger asChild>{trigger}</DrawerTrigger> : null}\n\n      {/*\n        We render DrawerContent so vaul's drag gestures still work, but we\n        layer motion.div overlays on top for the visual polish (backdrop blur,\n        spring slide-in, content stagger, handle pulse).\n      */}\n      <DrawerContent\n        className={cn(\n          // Hide vaul's default CSS transition — we animate with motion\n          \"!transition-none !duration-0\",\n          // Hide the default handle for bottom drawers — we render our own animated one\n          side === \"bottom\" &&\n            \"[&>[class*='h-2'][class*='rounded-full']]:hidden\",\n          className\n        )}\n      >\n        {/* Animated handle for bottom drawers */}\n        {side === \"bottom\" && (\n          <AnimatedHandle shouldReduceMotion={shouldReduceMotion} />\n        )}\n\n        <AnimatePresence onExitComplete={handleExitComplete}>\n          {open ? (\n            <motion.div\n              animate={shouldReduceMotion ? { opacity: 1 } : { opacity: 1 }}\n              exit={\n                shouldReduceMotion\n                  ? { opacity: 0, transition: { duration: 0 } }\n                  : { opacity: 0, transition: { duration: 0.15 } }\n              }\n              initial={shouldReduceMotion ? { opacity: 1 } : { opacity: 0 }}\n              transition={{\n                duration: shouldReduceMotion ? 0 : BACKDROP_DURATION,\n              }}\n            >\n              {/* Staggered header */}\n              {title || description ? (\n                <StaggerChild index={0} shouldReduceMotion={shouldReduceMotion}>\n                  <DrawerHeader>\n                    {title ? <DrawerTitle>{title}</DrawerTitle> : null}\n                    {description ? (\n                      <DrawerDescription>{description}</DrawerDescription>\n                    ) : null}\n                  </DrawerHeader>\n                </StaggerChild>\n              ) : null}\n\n              {/* Staggered body */}\n              {children ? (\n                <StaggerChild\n                  index={title || description ? 1 : 0}\n                  shouldReduceMotion={shouldReduceMotion}\n                >\n                  <div className=\"px-4\">{children}</div>\n                </StaggerChild>\n              ) : null}\n\n              {/* Staggered footer */}\n              {footer ? (\n                <StaggerChild\n                  index={(title || description ? 1 : 0) + (children ? 1 : 0)}\n                  shouldReduceMotion={shouldReduceMotion}\n                >\n                  <DrawerFooter>{footer}</DrawerFooter>\n                </StaggerChild>\n              ) : null}\n            </motion.div>\n          ) : null}\n        </AnimatePresence>\n      </DrawerContent>\n    </DrawerPrimitive>\n  );\n}\n\n/* ------------------------------------------------------------------ */\n/*  Re-exports                                                         */\n/* ------------------------------------------------------------------ */\n\nexport {\n  DrawerClose,\n  DrawerContent,\n  DrawerDescription,\n  DrawerFooter,\n  DrawerHeader,\n  DrawerTitle,\n  DrawerTrigger,\n};\n","path":"index.tsx","target":"components/smoothui/drawer/index.tsx","type":"registry:ui"}],"name":"drawer","registryDependencies":["drawer"],"title":"Drawer","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"An animated Dropdown Menu component for SmoothUI with spring animations, nested submenus, and keyboard navigation.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport {\n  DropdownMenuContent,\n  DropdownMenuGroup,\n  DropdownMenuItem,\n  DropdownMenuLabel,\n  DropdownMenu as DropdownMenuRoot,\n  DropdownMenuSeparator,\n  DropdownMenuShortcut,\n  DropdownMenuSub,\n  DropdownMenuSubContent,\n  DropdownMenuSubTrigger,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport { cn } from \"@/lib/utils\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport type React from \"react\";\nimport { useState } from \"react\";\nimport { SPRING_DEFAULT } from \"@/components/smoothui/lib/animation\";\n\nexport interface DropdownMenuProps {\n  /** Alignment of the dropdown relative to the trigger */\n  align?: \"start\" | \"center\" | \"end\";\n  /** The trigger element that opens the menu */\n  children: React.ReactNode;\n  /** Optional CSS class for the content container */\n  className?: string;\n  /** Menu items to render */\n  items: DropdownMenuItemConfig[];\n  /** Callback when open state changes */\n  onOpenChange?: (open: boolean) => void;\n  /** Controlled open state */\n  open?: boolean;\n  /** Side offset for the dropdown content */\n  sideOffset?: number;\n}\n\nexport interface DropdownMenuItemConfig {\n  /** Nested submenu items */\n  children?: DropdownMenuItemConfig[];\n  /** Whether the item is disabled */\n  disabled?: boolean;\n  /** Renders a group label instead of an item */\n  groupLabel?: string;\n  /** Optional icon to display before the label */\n  icon?: React.ReactNode;\n  /** Unique key for the item */\n  key: string;\n  /** Display label */\n  label: string;\n  /** Callback when item is selected */\n  onSelect?: () => void;\n  /** Renders a separator instead of an item */\n  separator?: boolean;\n  /** Optional keyboard shortcut text to display */\n  shortcut?: string;\n  /** Destructive variant styling */\n  variant?: \"default\" | \"destructive\";\n}\n\nexport default function DropdownMenu({\n  children,\n  items,\n  className,\n  open,\n  onOpenChange,\n  sideOffset = 4,\n  align = \"start\",\n}: DropdownMenuProps) {\n  const shouldReduceMotion = useReducedMotion();\n  const [isOpen, setIsOpen] = useState(false);\n\n  const controlledOpen = open ?? isOpen;\n  const handleOpenChange = (value: boolean) => {\n    setIsOpen(value);\n    onOpenChange?.(value);\n  };\n\n  const renderItem = (item: DropdownMenuItemConfig, index: number) => {\n    if (item.separator) {\n      return <DropdownMenuSeparator key={item.key} />;\n    }\n\n    if (item.groupLabel) {\n      return (\n        <DropdownMenuLabel key={item.key}>{item.groupLabel}</DropdownMenuLabel>\n      );\n    }\n\n    if (item.children && item.children.length > 0) {\n      return (\n        <DropdownMenuSub key={item.key}>\n          <DropdownMenuSubTrigger disabled={item.disabled}>\n            {item.icon ? <span className=\"mr-2\">{item.icon}</span> : null}\n            {item.label}\n          </DropdownMenuSubTrigger>\n          <DropdownMenuSubContent>\n            {item.children.map((child, childIndex) =>\n              renderItem(child, childIndex)\n            )}\n          </DropdownMenuSubContent>\n        </DropdownMenuSub>\n      );\n    }\n\n    return (\n      <motion.div\n        animate={shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }}\n        initial={shouldReduceMotion ? { opacity: 1 } : { opacity: 0, y: -4 }}\n        key={item.key}\n        transition={\n          shouldReduceMotion\n            ? { duration: 0 }\n            : { ...SPRING_DEFAULT, delay: index * 0.02 }\n        }\n      >\n        <DropdownMenuItem\n          disabled={item.disabled}\n          onSelect={item.onSelect}\n          variant={item.variant}\n        >\n          {item.icon ? <span className=\"mr-2\">{item.icon}</span> : null}\n          {item.label}\n          {item.shortcut ? (\n            <DropdownMenuShortcut>{item.shortcut}</DropdownMenuShortcut>\n          ) : null}\n        </DropdownMenuItem>\n      </motion.div>\n    );\n  };\n\n  return (\n    <DropdownMenuRoot onOpenChange={handleOpenChange} open={controlledOpen}>\n      <DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>\n      <DropdownMenuContent\n        align={align}\n        className={cn(\"origin-top\", className)}\n        sideOffset={sideOffset}\n      >\n        <motion.div\n          animate={\n            shouldReduceMotion ? { opacity: 1 } : { opacity: 1, scale: 1, y: 0 }\n          }\n          initial={\n            shouldReduceMotion\n              ? { opacity: 1 }\n              : { opacity: 0, scale: 0.95, y: -4 }\n          }\n          transition={shouldReduceMotion ? { duration: 0 } : SPRING_DEFAULT}\n        >\n          <DropdownMenuGroup>\n            {items.map((item, index) => renderItem(item, index))}\n          </DropdownMenuGroup>\n        </motion.div>\n      </DropdownMenuContent>\n    </DropdownMenuRoot>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/dropdown-menu/index.tsx","type":"registry:ui"}],"name":"dropdown-menu","registryDependencies":["dropdown-menu","https://smoothui.dev/r/lib.json"],"title":"Dropdown Menu","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion","lucide-react"],"description":"A DynamicIsland component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport {\n  Bell,\n  CloudLightning,\n  Music2,\n  Pause,\n  Phone,\n  Play,\n  SkipBack,\n  SkipForward,\n  Thermometer,\n  Timer as TimerIcon,\n} from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { type ReactNode, useMemo, useState } from \"react\";\n\nconst BOUNCE_VARIANTS = {\n  idle: 0.5,\n  \"idle-ring\": 0.5,\n  \"idle-timer\": 0.3,\n  \"ring-idle\": 0.5,\n  \"ring-timer\": 0.35,\n  \"timer-idle\": 0.3,\n  \"timer-ring\": 0.35,\n} as const;\n\nconst DEFAULT_BOUNCE = 0.5;\nconst TIMER_INTERVAL_MS = 1000;\n\n// Idle Component with Weather\nconst DefaultIdle = () => {\n  const [showTemp, setShowTemp] = useState(false);\n\n  return (\n    <motion.div\n      className=\"flex items-center gap-2 px-3 py-2\"\n      layout\n      onHoverEnd={() => setShowTemp(false)}\n      onHoverStart={() => setShowTemp(true)}\n    >\n      <AnimatePresence mode=\"wait\">\n        <motion.div\n          animate={{ opacity: 1, scale: 1 }}\n          className=\"text-white\"\n          exit={{ opacity: 0, scale: 0.8 }}\n          initial={{ opacity: 0, scale: 0.8 }}\n          key=\"storm\"\n        >\n          <CloudLightning className=\"h-5 w-5 text-white\" />\n        </motion.div>\n      </AnimatePresence>\n\n      <AnimatePresence>\n        {showTemp ? (\n          <motion.div\n            animate={{ opacity: 1, width: \"auto\" }}\n            className=\"flex items-center gap-1 overflow-hidden text-white\"\n            exit={{ opacity: 0, width: 0 }}\n            initial={{ opacity: 0, width: 0 }}\n          >\n            <Thermometer className=\"h-3 w-3\" />\n            <span className=\"pointer-events-none whitespace-nowrap text-white text-xs\">\n              12°C\n            </span>\n          </motion.div>\n        ) : null}\n      </AnimatePresence>\n    </motion.div>\n  );\n};\n\n// Ring Component\nconst DefaultRing = () => (\n  <div className=\"flex w-64 items-center gap-3 overflow-hidden px-4 py-2 text-white\">\n    <Phone className=\"h-5 w-5 text-green-500\" />\n    <div className=\"flex-1\">\n      <p className=\"pointer-events-none font-medium text-sm text-white\">\n        Incoming Call\n      </p>\n      <p className=\"pointer-events-none text-white text-xs opacity-70\">\n        Guillermo Rauch\n      </p>\n    </div>\n    <div className=\"h-2 w-2 animate-pulse rounded-full bg-green-500\" />\n  </div>\n);\n\n// Timer Component\nconst DefaultTimer = () => {\n  const [time, setTime] = useState(60);\n\n  useMemo(() => {\n    const timer = setInterval(() => {\n      setTime((t) => (t > 0 ? t - 1 : 0));\n    }, TIMER_INTERVAL_MS);\n    return () => clearInterval(timer);\n  }, []);\n\n  return (\n    <div className=\"flex w-64 items-center gap-3 overflow-hidden px-4 py-2 text-white\">\n      <TimerIcon className=\"h-5 w-5 text-amber-500\" />\n      <div className=\"flex-1\">\n        <p className=\"pointer-events-none font-medium text-sm text-white\">\n          {time}s remaining\n        </p>\n      </div>\n      <div className=\"h-1 w-24 overflow-hidden rounded-full bg-white/20\">\n        <motion.div\n          animate={{ width: \"0%\" }}\n          className=\"h-full bg-amber-500\"\n          initial={{ width: \"100%\" }}\n          transition={{ duration: time, ease: \"linear\" }}\n        />\n      </div>\n    </div>\n  );\n};\n\n// Notification Component\nconst Notification = () => (\n  <div className=\"flex w-64 items-center gap-3 overflow-hidden px-4 py-2 text-white\">\n    <Bell className=\"h-5 w-5 text-yellow-400\" />\n    <div className=\"flex-1\">\n      <p className=\"pointer-events-none font-medium text-sm text-white\">\n        New Message\n      </p>\n      <p className=\"pointer-events-none text-white text-xs opacity-70\">\n        You have a new notification!\n      </p>\n    </div>\n    <span className=\"rounded-full bg-yellow-400/40 px-2 py-0.5 text-xs text-yellow-500\">\n      1\n    </span>\n  </div>\n);\n\n// Music Player Component\nconst MusicPlayer = () => {\n  const [playing, setPlaying] = useState(true);\n  return (\n    <div className=\"flex w-72 items-center gap-3 overflow-hidden px-4 py-2 text-white\">\n      <Music2 className=\"h-5 w-5 text-pink-500\" />\n      <div className=\"min-w-0 flex-1\">\n        <p className=\"pointer-events-none truncate font-medium text-sm text-white\">\n          Lofi Chill Beats\n        </p>\n        <p className=\"pointer-events-none truncate text-white text-xs opacity-70\">\n          DJ Smooth\n        </p>\n      </div>\n      <button\n        className=\"rounded-full p-1 hover:bg-white/30\"\n        onClick={() => setPlaying(false)}\n        type=\"button\"\n      >\n        <SkipBack className=\"h-4 w-4 text-white\" />\n      </button>\n      <button\n        className=\"rounded-full p-1 hover:bg-white/30\"\n        onClick={() => setPlaying((p) => !p)}\n        type=\"button\"\n      >\n        {playing ? (\n          <Pause className=\"h-4 w-4 text-white\" />\n        ) : (\n          <Play className=\"h-4 w-4 text-white\" />\n        )}\n      </button>\n      <button\n        className=\"rounded-full p-1 hover:bg-white/30\"\n        onClick={() => setPlaying(true)}\n        type=\"button\"\n      >\n        <SkipForward className=\"h-4 w-4 text-white\" />\n      </button>\n    </div>\n  );\n};\n\ntype View = \"idle\" | \"ring\" | \"timer\" | \"notification\" | \"music\";\n\nexport interface DynamicIslandProps {\n  className?: string;\n  idleContent?: ReactNode;\n  onViewChange?: (view: View) => void;\n  ringContent?: ReactNode;\n  timerContent?: ReactNode;\n  view?: View;\n}\n\nexport default function DynamicIsland({\n  view: controlledView,\n  onViewChange,\n  idleContent,\n  ringContent,\n  timerContent,\n  className = \"\",\n}: DynamicIslandProps) {\n  const [internalView, setInternalView] = useState<View>(\"idle\");\n  const [variantKey, setVariantKey] = useState<string>(\"idle\");\n  const shouldReduceMotion = useReducedMotion();\n\n  const view = controlledView ?? internalView;\n\n  const content = useMemo(() => {\n    switch (view) {\n      case \"ring\":\n        return ringContent ?? <DefaultRing />;\n      case \"timer\":\n        return timerContent ?? <DefaultTimer />;\n      case \"notification\":\n        return <Notification />;\n      case \"music\":\n        return <MusicPlayer />;\n      default:\n        return idleContent ?? <DefaultIdle />;\n    }\n  }, [view, idleContent, ringContent, timerContent]);\n\n  const handleViewChange = (newView: View) => {\n    if (view === newView) {\n      return;\n    }\n    setVariantKey(`${view}-${newView}`);\n    if (onViewChange) {\n      onViewChange(newView);\n    } else {\n      setInternalView(newView);\n    }\n  };\n\n  return (\n    <div className={`h-[200px] ${className}`}>\n      <div className=\"relative flex h-full w-full flex-col justify-center\">\n        <motion.div\n          className=\"mx-auto w-fit min-w-[100px] overflow-hidden rounded-full bg-black\"\n          layout\n          style={{ borderRadius: 32 }}\n          transition={\n            shouldReduceMotion\n              ? { duration: 0 }\n              : {\n                  bounce:\n                    BOUNCE_VARIANTS[\n                      variantKey as keyof typeof BOUNCE_VARIANTS\n                    ] ?? DEFAULT_BOUNCE,\n                  duration: 0.25,\n                  type: \"spring\" as const,\n                }\n          }\n        >\n          <motion.div\n            animate={\n              shouldReduceMotion\n                ? { opacity: 1, scale: 1 }\n                : {\n                    filter: \"blur(0px)\",\n                    opacity: 1,\n                    originX: 0.5,\n                    originY: 0.5,\n                    scale: 1,\n                    transition: { delay: 0.05 },\n                  }\n            }\n            initial={{\n              filter: \"blur(5px)\",\n              opacity: 0,\n              originX: 0.5,\n              originY: 0.5,\n              scale: 0.9,\n            }}\n            key={view}\n            transition={{\n              bounce:\n                BOUNCE_VARIANTS[variantKey as keyof typeof BOUNCE_VARIANTS] ??\n                DEFAULT_BOUNCE,\n              type: \"spring\" as const,\n            }}\n          >\n            {content}\n          </motion.div>\n        </motion.div>\n\n        <div className=\"absolute bottom-2 left-1/2 z-10 flex -translate-x-1/2 justify-center gap-1 rounded-full border bg-background p-1\">\n          {[\n            { icon: <CloudLightning className=\"size-3\" />, key: \"idle\" },\n            { icon: <Phone className=\"size-3\" />, key: \"ring\" },\n            { icon: <TimerIcon className=\"size-3\" />, key: \"timer\" },\n            { icon: <Bell className=\"size-3\" />, key: \"notification\" },\n            { icon: <Music2 className=\"size-3\" />, key: \"music\" },\n          ].map(({ key, icon }) => (\n            <button\n              aria-label={key}\n              className=\"flex size-8 cursor-pointer items-center justify-center rounded-full border bg-primary px-2\"\n              key={key}\n              onClick={() => {\n                if (view !== key) {\n                  setVariantKey(`${view}-${key}`);\n                  handleViewChange(key as View);\n                }\n              }}\n              type=\"button\"\n            >\n              {icon}\n            </button>\n          ))}\n        </div>\n      </div>\n    </div>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/dynamic-island/index.tsx","type":"registry:ui"}],"name":"dynamic-island","registryDependencies":[],"title":"Dynamic Island","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion","lucide-react"],"description":"A ExpandableCards component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { Play } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useEffect, useRef, useState } from \"react\";\n\nconst _AVATAR_SIZE = 96;\n// ease-out-quint for entering/exiting elements\nconst EASE_OUT_QUINT = [0.23, 1, 0.32, 1] as const;\n\nexport interface Card {\n  author?: {\n    name: string;\n    role: string;\n    image: string;\n  };\n  content: string;\n  id: number;\n  image: string;\n  title: string;\n}\n\nexport interface ExpandableCardsProps {\n  cardClassName?: string;\n  cards: Card[];\n  className?: string;\n  onSelect?: (id: number | null) => void;\n  selectedCard?: number | null;\n}\n\nexport default function ExpandableCards({\n  cards,\n  selectedCard: controlledSelected,\n  onSelect,\n  className = \"\",\n  cardClassName = \"\",\n}: ExpandableCardsProps) {\n  const [internalSelected, setInternalSelected] = useState<number | null>(null);\n  const scrollRef = useRef<HTMLDivElement>(null);\n  const shouldReduceMotion = useReducedMotion();\n\n  const selectedCard =\n    controlledSelected === undefined ? internalSelected : controlledSelected;\n\n  useEffect(() => {\n    if (scrollRef.current) {\n      const { scrollWidth } = scrollRef.current;\n      const { clientWidth } = scrollRef.current;\n      scrollRef.current.scrollLeft = (scrollWidth - clientWidth) / 2;\n    }\n  }, []);\n\n  const handleCardClick = (id: number) => {\n    if (selectedCard === id) {\n      if (onSelect) {\n        onSelect(null);\n      } else {\n        setInternalSelected(null);\n      }\n    } else {\n      if (onSelect) {\n        onSelect(id);\n      } else {\n        setInternalSelected(id);\n      }\n      // Center the clicked card in view\n      const cardElement = document.querySelector(`[data-card-id=\"${id}\"]`);\n      if (cardElement) {\n        cardElement.scrollIntoView({\n          behavior: \"smooth\",\n          block: \"nearest\",\n          inline: \"center\",\n        });\n      }\n    }\n  };\n\n  return (\n    <div\n      className={`flex w-full flex-col gap-4 overflow-scroll p-4 ${className}`}\n    >\n      <div\n        className=\"scrollbar-hide mx-auto flex overflow-x-auto pt-4 pb-8\"\n        ref={scrollRef}\n        style={{\n          scrollPaddingLeft: \"20%\",\n          scrollSnapType: \"x mandatory\",\n        }}\n      >\n        {cards.map((card) => (\n          <motion.div\n            animate={{\n              width: selectedCard === card.id ? \"500px\" : \"200px\",\n            }}\n            aria-label={`${card.title} card${selectedCard === card.id ? \", expanded\" : \"\"}`}\n            aria-selected={selectedCard === card.id}\n            className={`relative mr-4 h-[300px] shrink-0 cursor-pointer overflow-hidden rounded-2xl border bg-background shadow-lg focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ${cardClassName}`}\n            data-card-id={card.id}\n            key={card.id}\n            layout\n            onClick={() => handleCardClick(card.id)}\n            onKeyDown={(e) => {\n              if (e.key === \"Enter\" || e.key === \" \") {\n                e.preventDefault();\n                handleCardClick(card.id);\n              }\n            }}\n            role=\"button\"\n            style={{\n              scrollSnapAlign: \"start\",\n            }}\n            tabIndex={0}\n            transition={\n              shouldReduceMotion\n                ? { duration: 0 }\n                : {\n                    duration: 0.25,\n                    ease: EASE_OUT_QUINT,\n                  }\n            }\n          >\n            <div className=\"relative h-full w-[200px]\">\n              <img\n                alt={card.title}\n                className=\"h-full w-full object-cover\"\n                draggable={false}\n                height={300}\n                src={card.image || \"/placeholder.svg\"}\n                width={200}\n              />\n              <div className=\"absolute inset-0 bg-black/20\" />\n              <div className=\"absolute inset-0 flex flex-col justify-between p-6 text-white\">\n                <h2 className=\"font-bold text-2xl\">{card.title}</h2>\n                <div className=\"flex items-center gap-2\">\n                  <button\n                    aria-label={`Play video: ${card.title}`}\n                    className=\"ease flex h-12 min-h-[44px] w-12 min-w-[44px] items-center justify-center rounded-full bg-background/30 backdrop-blur-sm transition-transform duration-200 hover:scale-110 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\"\n                    onClick={(e) => {\n                      e.stopPropagation();\n                      // Handle play action\n                    }}\n                    type=\"button\"\n                  >\n                    <Play className=\"h-6 w-6 text-white\" />\n                  </button>\n                  <span className=\"font-medium text-sm\">Play video</span>\n                </div>\n              </div>\n            </div>\n            <AnimatePresence mode=\"popLayout\">\n              {selectedCard === card.id && (\n                <motion.div\n                  animate={\n                    shouldReduceMotion\n                      ? { opacity: 1, width: \"300px\" }\n                      : { filter: \"blur(0px)\", opacity: 1, width: \"300px\" }\n                  }\n                  className=\"absolute top-0 right-0 h-full bg-background\"\n                  exit={\n                    shouldReduceMotion\n                      ? { opacity: 0, width: 0 }\n                      : { filter: \"blur(5px)\", opacity: 0, width: 0 }\n                  }\n                  initial={\n                    shouldReduceMotion\n                      ? { opacity: 0, width: 0 }\n                      : { filter: \"blur(5px)\", opacity: 0, width: 0 }\n                  }\n                  transition={\n                    shouldReduceMotion\n                      ? { duration: 0 }\n                      : {\n                          duration: 0.25,\n                          ease: EASE_OUT_QUINT,\n                          opacity: { delay: 0.1, duration: 0.2 },\n                        }\n                  }\n                >\n                  <motion.div\n                    animate={\n                      shouldReduceMotion\n                        ? { opacity: 1, x: 0 }\n                        : { filter: \"blur(0px)\", opacity: 1, x: 0 }\n                    }\n                    className=\"flex h-full flex-col justify-between p-8\"\n                    exit={\n                      shouldReduceMotion\n                        ? { opacity: 0, x: 20 }\n                        : { filter: \"blur(5px)\", opacity: 0, x: 20 }\n                    }\n                    initial={\n                      shouldReduceMotion\n                        ? { opacity: 0, x: 20 }\n                        : { filter: \"blur(5px)\", opacity: 0, x: 20 }\n                    }\n                    transition={\n                      shouldReduceMotion\n                        ? { duration: 0 }\n                        : { delay: 0.2, duration: 0.2, ease: EASE_OUT_QUINT }\n                    }\n                  >\n                    <p className=\"text-primary-foreground text-sm\">\n                      {card.content}\n                    </p>\n                    {card.author ? (\n                      <div className=\"mt-4 flex items-center gap-3\">\n                        <div className=\"h-12 w-12 overflow-hidden rounded-full border bg-primary\">\n                          <img\n                            alt={card.author.name}\n                            className=\"h-full w-full object-cover\"\n                            draggable={false}\n                            height={48}\n                            src={card.author.image}\n                            width={48}\n                          />\n                        </div>\n                        <div>\n                          <p className=\"font-semibold text-foreground\">\n                            {card.author.name}\n                          </p>\n                          <p className=\"text-primary-foreground text-xs\">\n                            {card.author.role}\n                          </p>\n                        </div>\n                      </div>\n                    ) : null}\n                  </motion.div>\n                </motion.div>\n              )}\n            </AnimatePresence>\n          </motion.div>\n        ))}\n      </div>\n    </div>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/expandable-cards/index.tsx","type":"registry:ui"}],"name":"expandable-cards","registryDependencies":[],"title":"Expandable Cards","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"iOS-style exposure slider with draggable ticker and progress ring","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport {\n  type MotionValue,\n  motion,\n  useMotionValue,\n  useReducedMotion,\n  useSpring,\n  useTransform,\n} from \"motion/react\";\nimport { useCallback, useRef } from \"react\";\n\nexport interface ExposureSliderProps {\n  /** Accent color for the active notch and progress ring (CSS color value) */\n  accentColor?: string;\n  /** Additional CSS classes */\n  className?: string;\n  /** Initial value */\n  defaultValue?: number;\n  /** Maximum value */\n  max?: number;\n  /** Minimum value */\n  min?: number;\n  /** Callback fired when the value changes */\n  onChange?: (value: number) => void;\n  /** Show the circular progress indicator with the current value */\n  showIndicator?: boolean;\n  /** Step size between values */\n  step?: number;\n}\n\nconst NOTCH_WIDTH = 13; // px per notch (3px notch + 10px gap)\nconst SPRING_CONFIG = { damping: 30, mass: 0.5, stiffness: 300 };\n\nconst DEFAULT_ACCENT = \"var(--color-brand, oklch(0.65 0.25 12))\";\n\nconst ExposureSlider = ({\n  min = -20,\n  max = 20,\n  step = 1,\n  defaultValue = 0,\n  onChange,\n  showIndicator = true,\n  accentColor,\n  className,\n}: ExposureSliderProps) => {\n  const shouldReduceMotion = useReducedMotion();\n  const containerRef = useRef<HTMLDivElement>(null);\n  const isDragging = useRef(false);\n\n  const count = Math.floor((max - min) / step) + 1;\n  const centerIndex = Math.floor((defaultValue - min) / step);\n\n  // Raw drag offset and spring-smoothed version\n  const rawX = useMotionValue(0);\n  const x = shouldReduceMotion ? rawX : useSpring(rawX, SPRING_CONFIG);\n\n  // Current value derived from offset\n  const currentValue = useTransform(x, (latest) => {\n    const indexOffset = Math.round(-latest / NOTCH_WIDTH);\n    const val = Math.max(\n      min,\n      Math.min(max, (centerIndex + indexOffset) * step + min)\n    );\n    return val;\n  });\n\n  // Track value for circle and display\n  const displayValue = useTransform(currentValue, (v) => Math.round(v));\n  const normalizedValue = useTransform(currentValue, [min, max], [-1, 1]);\n\n  const snapToNearest = useCallback(() => {\n    const current = rawX.get();\n    const snapped = Math.round(current / NOTCH_WIDTH) * NOTCH_WIDTH;\n    rawX.set(snapped);\n  }, [rawX]);\n\n  const handlePointerDown = useCallback(\n    (e: React.PointerEvent) => {\n      isDragging.current = true;\n      const startX = e.clientX;\n      const startOffset = rawX.get();\n\n      const handleMove = (moveEvent: PointerEvent) => {\n        const delta = moveEvent.clientX - startX;\n        const newOffset = startOffset + delta;\n        // Clamp to bounds\n        const maxOffset = (count - 1 - centerIndex) * NOTCH_WIDTH;\n        const minOffset = -centerIndex * NOTCH_WIDTH;\n        rawX.set(Math.max(-maxOffset, Math.min(-minOffset, newOffset)));\n\n        // Fire onChange\n        const indexOffset = Math.round(-rawX.get() / NOTCH_WIDTH);\n        const val = Math.max(\n          min,\n          Math.min(max, (centerIndex + indexOffset) * step + min)\n        );\n        onChange?.(Math.round(val));\n      };\n\n      const handleUp = () => {\n        isDragging.current = false;\n        snapToNearest();\n        // Fire final onChange\n        const indexOffset = Math.round(-rawX.get() / NOTCH_WIDTH);\n        const val = Math.max(\n          min,\n          Math.min(max, (centerIndex + indexOffset) * step + min)\n        );\n        onChange?.(Math.round(val));\n        window.removeEventListener(\"pointermove\", handleMove);\n        window.removeEventListener(\"pointerup\", handleUp);\n      };\n\n      window.addEventListener(\"pointermove\", handleMove);\n      window.addEventListener(\"pointerup\", handleUp);\n    },\n    [rawX, centerIndex, count, min, max, step, onChange, snapToNearest]\n  );\n\n  const items = Array.from({ length: count }, (_, i) => i);\n\n  return (\n    <div\n      className={cn(\n        \"flex w-full max-w-[500px] flex-col items-center gap-6 text-foreground\",\n        className\n      )}\n      style={\n        { \"--es-accent\": accentColor ?? DEFAULT_ACCENT } as React.CSSProperties\n      }\n    >\n      {/* Progress circle */}\n      {showIndicator ? (\n        <ProgressCircle\n          displayValue={displayValue}\n          normalizedValue={normalizedValue}\n        />\n      ) : null}\n\n      {/* Ticker slider */}\n      <div\n        className=\"relative flex h-10 w-full items-center justify-center\"\n        style={{\n          maskImage:\n            \"linear-gradient(to right, transparent 0%, black 20%, black 80%, transparent 100%)\",\n          WebkitMaskImage:\n            \"linear-gradient(to right, transparent 0%, black 20%, black 80%, transparent 100%)\",\n        }}\n      >\n        <div\n          className=\"relative h-full w-full cursor-grab select-none active:cursor-grabbing\"\n          onPointerDown={handlePointerDown}\n          ref={containerRef}\n          style={{\n            padding: `0 calc(50% - ${NOTCH_WIDTH / 2}px)`,\n            touchAction: \"pan-y\",\n          }}\n        >\n          <motion.ul\n            className=\"relative m-0 flex h-full list-none items-center p-0\"\n            style={{ marginLeft: -centerIndex * NOTCH_WIDTH, x }}\n          >\n            {items.map((i) => (\n              <Notch centerIndex={centerIndex} index={i} key={i} x={x} />\n            ))}\n          </motion.ul>\n        </div>\n      </div>\n    </div>\n  );\n};\n\n/** Individual notch mark in the ticker */\nconst Notch = ({\n  index,\n  centerIndex,\n  x,\n}: {\n  index: number;\n  centerIndex: number;\n  x: MotionValue<number>;\n}) => {\n  // Distance from center in notch units\n  const distance = useTransform(x, (latest) => {\n    const currentCenter = centerIndex + -latest / NOTCH_WIDTH;\n    return Math.abs(index - currentCenter);\n  });\n\n  const opacity = useTransform(distance, [0, 1, 3], [1, 0.6, 0.3]);\n  const clipTop = useTransform(distance, [0, 1, 2], [0, 30, 50]);\n  const clipPath = useTransform(clipTop, (v) => `inset(${v}% 0px 0px)`);\n\n  const isCenter = useTransform(distance, (d) => d < 0.5);\n  const bg = useTransform(isCenter, (center) =>\n    center ? \"var(--es-accent)\" : \"currentColor\"\n  );\n\n  return (\n    <li\n      className=\"relative flex-shrink-0 flex-grow-0\"\n      style={{ height: \"fit-content\" }}\n    >\n      <div style={{ padding: \"0 5px\" }}>\n        <motion.div\n          className=\"rounded-sm\"\n          style={{\n            backgroundColor: bg,\n            clipPath,\n            height: 40,\n            opacity,\n            width: 3,\n            willChange: \"clip-path, opacity\",\n          }}\n        />\n      </div>\n    </li>\n  );\n};\n\n/** SVG progress ring showing positive/negative value */\nconst ProgressCircle = ({\n  normalizedValue,\n  displayValue,\n}: {\n  normalizedValue: MotionValue<number>;\n  displayValue: MotionValue<number>;\n}) => {\n  // Positive arc (right side, 0 to 1)\n  const positiveDash = useTransform(normalizedValue, (v) =>\n    v > 0 ? `${v} ${1 - v}` : \"0 1\"\n  );\n\n  // Negative arc (left side, mirrored)\n  const negativeDash = useTransform(normalizedValue, (v) =>\n    v < 0 ? `${-v} ${1 + v}` : \"0 1\"\n  );\n\n  // Color: accent when non-zero, foreground when zero\n  const color = useTransform(normalizedValue, (v) =>\n    Math.abs(v) > 0.01 ? \"var(--es-accent)\" : \"currentColor\"\n  );\n\n  return (\n    <div className=\"relative flex h-[75px] w-[75px] items-center justify-center\">\n      <svg className=\"absolute inset-0 h-full w-full\" viewBox=\"0 0 100 100\">\n        {/* Background ring */}\n        <circle\n          cx=\"50\"\n          cy=\"50\"\n          fill=\"currentColor\"\n          fillOpacity={0.067}\n          r=\"48\"\n          stroke=\"currentColor\"\n          strokeOpacity={0.3}\n          strokeWidth=\"3\"\n        />\n        {/* Positive indicator */}\n        <motion.circle\n          cx=\"50\"\n          cy=\"50\"\n          fill=\"none\"\n          pathLength={1}\n          r=\"48\"\n          stroke=\"var(--es-accent)\"\n          strokeDasharray={positiveDash}\n          strokeDashoffset={0}\n          strokeWidth=\"3\"\n          style={{\n            transform: \"rotate(-90deg)\",\n            transformBox: \"fill-box\",\n            transformOrigin: \"50% 50%\",\n          }}\n        />\n        {/* Negative indicator */}\n        <motion.circle\n          cx=\"50\"\n          cy=\"50\"\n          fill=\"none\"\n          pathLength={1}\n          r=\"48\"\n          stroke=\"var(--es-accent)\"\n          strokeDasharray={negativeDash}\n          strokeDashoffset={0}\n          strokeWidth=\"3\"\n          style={{\n            transform: \"scaleX(-1) rotate(-90deg)\",\n            transformBox: \"fill-box\",\n            transformOrigin: \"50% 50%\",\n          }}\n        />\n      </svg>\n      <motion.span className=\"absolute font-semibold text-lg\" style={{ color }}>\n        {displayValue}\n      </motion.span>\n    </div>\n  );\n};\n\nexport default ExposureSlider;\n","path":"index.tsx","target":"components/smoothui/exposure-slider/index.tsx","type":"registry:ui"}],"name":"exposure-slider","registryDependencies":["https://smoothui.dev/r/tokens.json"],"title":"Exposure Slider","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"A FadeThrough text animation component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useEffect, useState } from \"react\";\n\nexport interface FadeThroughProps {\n  className?: string;\n  /** Interval between phrase transitions, in milliseconds. */\n  interval?: number;\n  phrases: string[];\n}\n\nconst ENTER_DURATION_S = 0.42;\nconst EXIT_DURATION_S = 0.26;\nconst ENTER_EASE = [0.2, 0, 0, 1] as const;\nconst EXIT_EASE = [0.4, 0, 1, 1] as const;\n\n/**\n * FadeThrough — Material-style phrase cycling: old text fades out,\n * new text fades in with a soft delay. From the animate-text catalog\n * (`fade-through`). Best for hero copy swaps and rotating taglines.\n */\nexport default function FadeThrough({\n  phrases,\n  className = \"\",\n  interval = 2500,\n}: FadeThroughProps) {\n  const shouldReduceMotion = useReducedMotion();\n  const [index, setIndex] = useState(0);\n\n  useEffect(() => {\n    if (shouldReduceMotion || phrases.length <= 1) {\n      return;\n    }\n    const id = setInterval(() => {\n      setIndex((prev) => (prev + 1) % phrases.length);\n    }, interval);\n    return () => clearInterval(id);\n  }, [shouldReduceMotion, phrases.length, interval]);\n\n  const current = phrases[index] ?? \"\";\n\n  return (\n    <span\n      aria-live=\"polite\"\n      className={className}\n      style={{ display: \"inline-block\", position: \"relative\" }}\n    >\n      <AnimatePresence mode=\"wait\">\n        <motion.span\n          animate={{ filter: \"blur(0px)\", opacity: 1, scale: 1, y: 0 }}\n          exit={{\n            filter: \"blur(0px)\",\n            opacity: 0,\n            scale: 1,\n            transition: { duration: EXIT_DURATION_S, ease: EXIT_EASE },\n            y: -4,\n          }}\n          initial={\n            shouldReduceMotion\n              ? { opacity: 1 }\n              : { filter: \"blur(2px)\", opacity: 0, scale: 0.99, y: 6 }\n          }\n          key={index}\n          style={{ display: \"inline-block\" }}\n          transition={\n            shouldReduceMotion\n              ? { duration: 0 }\n              : { duration: ENTER_DURATION_S, ease: ENTER_EASE }\n          }\n        >\n          {current}\n        </motion.span>\n      </AnimatePresence>\n    </span>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/fade-through/index.tsx","type":"registry:ui"}],"name":"fade-through","registryDependencies":[],"title":"Fade Through","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"A FigmaComment component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport {\n  Avatar,\n  AvatarFallback,\n  AvatarImage,\n} from \"@/components/ui/avatar\";\nimport { cn } from \"@/lib/utils\";\nimport { getImageKitUrl } from \"@/lib/smoothui-data\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { useClickOutside } from \"./use-click-outside\";\n\nconst CLOSED_SIZE = 32;\nconst AVATAR_CLOSED_LEFT = 4;\nconst AVATAR_CLOSED_TOP = 4;\nconst AVATAR_OPEN_LEFT = 12;\nconst AVATAR_OPEN_TOP = 12;\nconst CONTENT_DELAY = 0.15;\nconst INITIAL_BLUR_PX = 6;\nconst EXIT_BLUR_PX = 3;\nconst MEASURE_DELAY_SHORT = 100;\nconst MEASURE_DELAY_LONG = 500;\nconst CONTAINER_CLOSE_DELAY = 0.08;\nconst _BLUR_DURATION = 0.4;\nconst BLUR_EASE_X1 = 0.22;\nconst BLUR_EASE_Y1 = 1;\nconst BLUR_EASE_X2 = 0.36;\nconst BLUR_EASE_Y2 = 1;\nconst BLUR_EASE: [number, number, number, number] = [\n  BLUR_EASE_X1,\n  BLUR_EASE_Y1,\n  BLUR_EASE_X2,\n  BLUR_EASE_Y2,\n];\n\nexport interface FigmaCommentProps {\n  authorName?: string;\n  avatarAlt?: string;\n  avatarUrl?: string;\n  className?: string;\n  message?: string;\n  onOpenChange?: (isOpen: boolean) => void;\n  timestamp?: string;\n  width?: number;\n}\n\nexport default function FigmaComment({\n  avatarUrl = getImageKitUrl(\n    \"https://ik.imagekit.io/16u211libb/avatar-educalvolpz.jpeg?updatedAt=1765524159631\",\n    {\n      format: \"auto\",\n      height: 48,\n      quality: 85,\n      width: 48,\n    }\n  ),\n  avatarAlt = \"Avatar\",\n  className,\n  authorName = \"Edu Calvo\",\n  timestamp = \"Just now\",\n  message = \"What happens if we adjust this to handle a light and dark mode? I'm not sure if we're ready to handle...\",\n  width = 180,\n  onOpenChange,\n}: FigmaCommentProps) {\n  const [isOpen, setIsOpen] = useState(false);\n  const contentRef = useRef<HTMLDivElement>(null);\n  const containerRef = useRef<HTMLDivElement>(null);\n  const [contentHeight, setContentHeight] = useState(CLOSED_SIZE);\n  const shouldReduceMotion = useReducedMotion();\n\n  // Close comment when clicking outside\n  useClickOutside(containerRef, () => {\n    if (isOpen) {\n      setIsOpen(false);\n    }\n  });\n\n  // Notify parent of open state changes\n  useEffect(() => {\n    onOpenChange?.(isOpen);\n  }, [isOpen, onOpenChange]);\n\n  // Measure content height when component mounts or message changes\n  useEffect(() => {\n    const measureHeight = () => {\n      if (contentRef.current) {\n        const innerDiv = contentRef.current.firstElementChild as HTMLElement;\n        if (innerDiv) {\n          const height = innerDiv.scrollHeight;\n          if (height > 0) {\n            setContentHeight(height);\n          }\n        }\n      }\n    };\n\n    // Use setTimeout to ensure DOM is fully rendered\n    const timeoutId = setTimeout(measureHeight, MEASURE_DELAY_SHORT);\n    const timeoutId2 = setTimeout(measureHeight, MEASURE_DELAY_LONG);\n    return () => {\n      clearTimeout(timeoutId);\n      clearTimeout(timeoutId2);\n    };\n  }, []);\n\n  const handleToggle = () => {\n    setIsOpen((prev) => !prev);\n  };\n\n  return (\n    <div className={cn(\"relative\", className)}>\n      <motion.div\n        animate={\n          shouldReduceMotion\n            ? {}\n            : {\n                height: isOpen ? contentHeight : CLOSED_SIZE,\n                width: isOpen ? width : CLOSED_SIZE,\n              }\n        }\n        className=\"absolute bottom-0 left-0 cursor-pointer overflow-hidden rounded-2xl rounded-bl-none bg-background shadow-[0px_0px_0.5px_0px_rgba(0,0,0,0.18),0px_3px_8px_0px_rgba(0,0,0,0.1),0px_1px_3px_0px_rgba(0,0,0,0.1)]\"\n        onClick={handleToggle}\n        ref={containerRef}\n        style={\n          shouldReduceMotion\n            ? {\n                height: isOpen ? contentHeight : CLOSED_SIZE,\n                width: isOpen ? width : CLOSED_SIZE,\n              }\n            : undefined\n        }\n        transition={\n          shouldReduceMotion\n            ? { duration: 0 }\n            : {\n                damping: 45,\n                delay: isOpen ? 0 : CONTAINER_CLOSE_DELAY,\n                duration: 0.25,\n                mass: 0.7,\n                stiffness: 550,\n                type: \"spring\" as const,\n              }\n        }\n      >\n        {/* Avatar - animates position */}\n        <motion.div\n          animate={\n            shouldReduceMotion\n              ? {}\n              : {\n                  left: isOpen ? AVATAR_OPEN_LEFT : AVATAR_CLOSED_LEFT,\n                  top: isOpen ? AVATAR_OPEN_TOP : AVATAR_CLOSED_TOP,\n                }\n          }\n          className=\"absolute z-10\"\n          style={\n            shouldReduceMotion\n              ? {\n                  left: isOpen ? AVATAR_OPEN_LEFT : AVATAR_CLOSED_LEFT,\n                  top: isOpen ? AVATAR_OPEN_TOP : AVATAR_CLOSED_TOP,\n                }\n              : undefined\n          }\n          transition={\n            shouldReduceMotion\n              ? { duration: 0 }\n              : {\n                  damping: 25,\n                  duration: 0.25,\n                  stiffness: 300,\n                  type: \"spring\" as const,\n                }\n          }\n        >\n          <Avatar className=\"h-6 w-6\">\n            <AvatarImage alt={avatarAlt} src={avatarUrl} />\n            <AvatarFallback>{authorName.charAt(0)}</AvatarFallback>\n          </Avatar>\n        </motion.div>\n\n        {/* Content - always rendered but hidden when closed for measurement */}\n        <div\n          className=\"pointer-events-none absolute\"\n          ref={contentRef}\n          style={{\n            left: 0,\n            position: \"absolute\",\n            top: \"-9999px\",\n            width: `${width}px`,\n          }}\n        >\n          <div className=\"flex flex-col items-start gap-0.5 py-3 pr-4 pl-11\">\n            {/* Attribution */}\n            <div className=\"flex items-start gap-0.5\">\n              <p className=\"font-semibold text-[11px] text-foreground leading-4\">\n                {authorName}\n              </p>\n              <p className=\"font-medium text-[11px] text-muted-foreground leading-4\">\n                {timestamp}\n              </p>\n            </div>\n            {/* Message */}\n            <p className=\"text-left font-medium text-[11px] text-foreground leading-4\">\n              {message}\n            </p>\n          </div>\n        </div>\n\n        {/* Content - visible when open */}\n        <AnimatePresence>\n          {isOpen ? (\n            <motion.div\n              animate={\n                shouldReduceMotion\n                  ? { opacity: 1 }\n                  : {\n                      filter: \"blur(0px)\",\n                      opacity: 1,\n                    }\n              }\n              className=\"absolute inset-0 flex flex-col items-start gap-0.5 py-3 pr-4 pl-11\"\n              exit={\n                shouldReduceMotion\n                  ? { opacity: 0, transition: { duration: 0 } }\n                  : {\n                      filter: `blur(${String(EXIT_BLUR_PX)}px)`,\n                      opacity: 0,\n                    }\n              }\n              initial={\n                shouldReduceMotion\n                  ? { opacity: 0 }\n                  : {\n                      filter: `blur(${String(INITIAL_BLUR_PX)}px)`,\n                      opacity: 0,\n                    }\n              }\n              style={{\n                width: `${width}px`,\n              }}\n              transition={\n                (shouldReduceMotion\n                  ? { duration: 0 }\n                  : (isExiting: boolean) => ({\n                      filter: {\n                        delay: isExiting ? 0 : CONTENT_DELAY,\n                        duration: 0.25,\n                        ease: BLUR_EASE,\n                      },\n                      opacity: {\n                        delay: isExiting ? 0 : CONTENT_DELAY,\n                        duration: 0.25,\n                        ease: BLUR_EASE,\n                      },\n                    })) as import(\"motion/react\").Transition\n              }\n            >\n              {/* Attribution */}\n              <div className=\"flex items-start gap-0.5\">\n                <p className=\"font-semibold text-[11px] text-foreground leading-4\">\n                  {authorName}\n                </p>\n                <p className=\"font-medium text-[11px] text-muted-foreground leading-4\">\n                  {timestamp}\n                </p>\n              </div>\n              {/* Message */}\n              <p className=\"text-left font-medium text-[11px] text-foreground leading-4\">\n                {message}\n              </p>\n            </motion.div>\n          ) : null}\n        </AnimatePresence>\n      </motion.div>\n    </div>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/figma-comment/index.tsx","type":"registry:ui"},{"content":"\"use client\";\n\nimport { type RefObject, useEffect } from \"react\";\n\ntype EventType =\n  | \"mousedown\"\n  | \"mouseup\"\n  | \"touchstart\"\n  | \"touchend\"\n  | \"focusin\"\n  | \"focusout\";\n\nexport function useClickOutside<T extends HTMLElement = HTMLElement>(\n  ref: RefObject<T | null> | RefObject<T | null>[],\n  handler: (event: Event) => void,\n  eventType: EventType = \"mousedown\"\n): void {\n  useEffect(() => {\n    function callback(event: Event) {\n      const target = event.target as Node;\n\n      // Do nothing if the target is not connected element with document\n      if (!target?.isConnected) {\n        return;\n      }\n\n      const isOutside = Array.isArray(ref)\n        ? ref\n            .filter((r) => Boolean(r.current))\n            .every((r) => r.current && !r.current.contains(target))\n        : ref.current && !ref.current.contains(target);\n\n      if (isOutside) {\n        handler(event);\n      }\n    }\n\n    window.addEventListener(eventType, callback);\n\n    return () => {\n      window.removeEventListener(eventType, callback);\n    };\n  }, [ref, handler, eventType]);\n}\n","path":"use-click-outside.tsx","target":"components/smoothui/figma-comment/use-click-outside.tsx","type":"registry:ui"}],"name":"figma-comment","registryDependencies":["avatar","https://smoothui.dev/r/data.json"],"title":"Figma Comment","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"A FocusBlurResolve text animation component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { motion, useInView, useReducedMotion } from \"motion/react\";\nimport { useRef } from \"react\";\n\nexport interface FocusBlurResolveProps {\n  children: string;\n  className?: string;\n  /** Delay before the animation starts, in milliseconds. */\n  delay?: number;\n  /** Animate only once the text scrolls into view. */\n  triggerOnView?: boolean;\n}\n\nconst DURATION_S = 0.76;\nconst MS = 1000;\n// Smooth deceleration — premium focus-pull ease-out.\nconst EASE = [0.22, 1, 0.36, 1] as const;\n\n/**\n * FocusBlurResolve — whole-text entrance that pulls from heavy blur to crisp,\n * with a subtle upward drift and scale settle. Ideal for hero headlines and\n * section titles where the moment of resolution feels intentional.\n * From the animate-text catalog (`focus-blur-resolve`).\n */\nexport default function FocusBlurResolve({\n  children,\n  className = \"\",\n  delay = 0,\n  triggerOnView = false,\n}: FocusBlurResolveProps) {\n  const ref = useRef<HTMLSpanElement>(null);\n  const inView = useInView(ref, { once: true });\n  const shouldReduceMotion = useReducedMotion();\n  const play = (!triggerOnView || inView) && !shouldReduceMotion;\n\n  return (\n    <span aria-label={children} className={className} ref={ref}>\n      <motion.span\n        animate={\n          play ? { filter: \"blur(0px)\", opacity: 1, scale: 1, y: 0 } : undefined\n        }\n        aria-hidden=\"true\"\n        initial={\n          shouldReduceMotion\n            ? { filter: \"blur(0px)\", opacity: 1, scale: 1, y: 0 }\n            : { filter: \"blur(14px)\", opacity: 0, scale: 1.01, y: 14 }\n        }\n        style={{ display: \"inline-block\" }}\n        transition={\n          shouldReduceMotion\n            ? { duration: 0 }\n            : {\n                delay: delay / MS,\n                duration: DURATION_S,\n                ease: EASE,\n              }\n        }\n      >\n        {children}\n      </motion.span>\n    </span>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/focus-blur-resolve/index.tsx","type":"registry:ui"}],"name":"focus-blur-resolve","registryDependencies":[],"title":"Focus Blur Resolve","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["lucide-react","motion"],"description":"A lightweight, composable Form component with animated error messages and full accessibility support.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { Check } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport type React from \"react\";\nimport {\n  cloneElement,\n  createContext,\n  useCallback,\n  useContext,\n  useEffect,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport {\n  DURATION_INSTANT,\n  SPRING_DEFAULT,\n  SPRING_SNAPPY,\n} from \"@/components/smoothui/lib/animation\";\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst STAGGER_DELAY = 0.04;\nconst SHAKE_KEYFRAMES = [0, -6, 5, -4, 3, -1, 0];\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type FormErrors = Record<string, string | undefined>;\n\nexport interface FormProps extends React.ComponentProps<\"form\"> {\n  /** Form contents */\n  children: React.ReactNode;\n  /** Optional CSS class */\n  className?: string;\n  /** External errors object (e.g. from react-hook-form's `formState.errors`) */\n  errors?: FormErrors;\n  /** Callback invoked on native form submit with current errors map */\n  onFormSubmit?: (e: React.FormEvent<HTMLFormElement>) => void;\n}\n\nexport interface FormFieldProps {\n  /** Field contents (label, input, message) */\n  children: React.ReactNode;\n  /** Optional CSS class for the field wrapper */\n  className?: string;\n  /** Unique field name — used to look up errors */\n  name: string;\n}\n\nexport interface FormLabelProps extends React.ComponentProps<\"label\"> {\n  /** Label text */\n  children: React.ReactNode;\n  /** Optional CSS class */\n  className?: string;\n}\n\nexport interface FormMessageProps {\n  /** Override the error message (otherwise pulled from FormField context) */\n  children?: React.ReactNode;\n  /** Optional CSS class */\n  className?: string;\n}\n\nexport interface FormDescriptionProps extends React.ComponentProps<\"p\"> {\n  /** Description text */\n  children: React.ReactNode;\n  /** Optional CSS class */\n  className?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Context\n// ---------------------------------------------------------------------------\n\ninterface FormContextValue {\n  errors: FormErrors;\n  prevErrors: FormErrors;\n  submitCount: number;\n}\n\ninterface FormFieldContextValue {\n  error: string | undefined;\n  fieldIndex: number;\n  formDescriptionId: string;\n  formItemId: string;\n  formMessageId: string;\n  id: string;\n  name: string;\n  prevError: string | undefined;\n  submitCount: number;\n}\n\nconst FormContext = createContext<FormContextValue>({\n  errors: {},\n  prevErrors: {},\n  submitCount: 0,\n});\nconst FormFieldContext = createContext<FormFieldContextValue | null>(null);\n\nconst useFormCtx = () => useContext(FormContext);\n\nconst useFormFieldCtx = () => {\n  const ctx = useContext(FormFieldContext);\n  if (!ctx) {\n    throw new Error(\"FormLabel / FormMessage must be used inside <FormField>\");\n  }\n  return ctx;\n};\n\n// ---------------------------------------------------------------------------\n// Form\n// ---------------------------------------------------------------------------\n\nexport default function Form({\n  errors = {},\n  onFormSubmit,\n  className,\n  children,\n  ...props\n}: FormProps) {\n  const [submitCount, setSubmitCount] = useState(0);\n  const prevErrorsRef = useRef<FormErrors>({});\n  const [prevErrors, setPrevErrors] = useState<FormErrors>({});\n\n  const ctxValue = useMemo(\n    () => ({ errors, prevErrors, submitCount }),\n    [errors, submitCount, prevErrors]\n  );\n\n  const handleSubmit = useCallback(\n    (e: React.FormEvent<HTMLFormElement>) => {\n      setPrevErrors(prevErrorsRef.current);\n      prevErrorsRef.current = errors;\n      setSubmitCount((c) => c + 1);\n      if (onFormSubmit) {\n        onFormSubmit(e);\n      }\n    },\n    [onFormSubmit, errors]\n  );\n\n  return (\n    <FormContext.Provider value={ctxValue}>\n      <form\n        className={cn(\"grid gap-3\", className)}\n        noValidate\n        onSubmit={handleSubmit}\n        {...props}\n      >\n        {children}\n      </form>\n    </FormContext.Provider>\n  );\n}\n\n// ---------------------------------------------------------------------------\n// FormField — staggered entrance + validation shake\n// ---------------------------------------------------------------------------\n\nlet fieldCounter = 0;\n\nexport function FormField({ name, className, children }: FormFieldProps) {\n  const { errors, submitCount, prevErrors } = useFormCtx();\n  const id = useId();\n  const error = errors[name];\n  const prevError = prevErrors[name];\n\n  // Stable field index for stagger animation\n  const fieldIndexRef = useRef<number | null>(null);\n  if (fieldIndexRef.current === null) {\n    fieldIndexRef.current = fieldCounter;\n    fieldCounter += 1;\n  }\n\n  // Reset counter on unmount of the first field (index 0)\n  useEffect(\n    () => () => {\n      if (fieldIndexRef.current === 0) {\n        fieldCounter = 0;\n      }\n    },\n    []\n  );\n\n  const ctxValue = useMemo(\n    () => ({\n      error,\n      fieldIndex: fieldIndexRef.current ?? 0,\n      formDescriptionId: `${id}-form-item-description`,\n      formItemId: `${id}-form-item`,\n      formMessageId: `${id}-form-item-message`,\n      id,\n      name,\n      prevError,\n      submitCount,\n    }),\n    [name, id, error, submitCount, prevError]\n  );\n\n  return (\n    <FormFieldContext.Provider value={ctxValue}>\n      <FormFieldInner className={className}>{children}</FormFieldInner>\n    </FormFieldContext.Provider>\n  );\n}\n\n/** Inner component that can consume FormFieldContext */\nfunction FormFieldInner({\n  className,\n  children,\n}: {\n  className?: string;\n  children: React.ReactNode;\n}) {\n  const shouldReduceMotion = useReducedMotion();\n  const { error, fieldIndex, submitCount, prevError } = useFormFieldCtx();\n\n  // Shake when a new error appears on submit\n  const shouldShake = error && submitCount > 0;\n  const [shakeKey, setShakeKey] = useState(0);\n\n  useEffect(() => {\n    if (shouldShake) {\n      setShakeKey((k) => k + 1);\n    }\n  }, [shouldShake, submitCount]);\n\n  return (\n    <motion.div\n      animate={shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }}\n      className={cn(\"grid gap-1.5\", className)}\n      data-slot=\"form-field\"\n      initial={shouldReduceMotion ? { opacity: 1 } : { opacity: 0, y: 8 }}\n      transition={\n        shouldReduceMotion\n          ? DURATION_INSTANT\n          : {\n              ...SPRING_DEFAULT,\n              delay: fieldIndex * STAGGER_DELAY,\n            }\n      }\n    >\n      <motion.div\n        animate={\n          shouldShake && !shouldReduceMotion ? { x: SHAKE_KEYFRAMES } : { x: 0 }\n        }\n        className=\"grid gap-1.5\"\n        key={shakeKey}\n        transition={\n          shouldReduceMotion\n            ? DURATION_INSTANT\n            : { duration: 0.4, ease: [0.36, 0.07, 0.19, 0.97] }\n        }\n      >\n        {children}\n      </motion.div>\n    </motion.div>\n  );\n}\n\n// ---------------------------------------------------------------------------\n// FormLabel\n// ---------------------------------------------------------------------------\n\nexport function FormLabel({ className, children, ...props }: FormLabelProps) {\n  const { formItemId, error } = useFormFieldCtx();\n\n  return (\n    <label\n      className={cn(\n        \"font-medium text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\",\n        error && \"text-destructive\",\n        className\n      )}\n      data-slot=\"form-label\"\n      htmlFor={formItemId}\n      {...props}\n    >\n      {children}\n    </label>\n  );\n}\n\n// ---------------------------------------------------------------------------\n// FormControl — renders a wrapper with animated focus ring\n// ---------------------------------------------------------------------------\n\nexport function FormControl({\n  children,\n  className,\n}: {\n  children: React.ReactNode;\n  className?: string;\n}) {\n  const shouldReduceMotion = useReducedMotion();\n  const { formItemId, formDescriptionId, formMessageId, error } =\n    useFormFieldCtx();\n  const [isFocused, setIsFocused] = useState(false);\n\n  return (\n    <motion.div\n      animate={\n        shouldReduceMotion\n          ? {}\n          : {\n              boxShadow: isFocused\n                ? \"0 0 0 3px hsl(var(--ring) / 0.3)\"\n                : \"0 0 0 0px hsl(var(--ring) / 0)\",\n            }\n      }\n      className={cn(\"rounded-md\", className)}\n      data-slot=\"form-control\"\n      onBlur={() => setIsFocused(false)}\n      onFocus={() => setIsFocused(true)}\n      transition={shouldReduceMotion ? DURATION_INSTANT : SPRING_SNAPPY}\n    >\n      {cloneChildWithA11y(children, {\n        \"aria-describedby\": error\n          ? `${formDescriptionId} ${formMessageId}`\n          : formDescriptionId,\n        \"aria-invalid\": error ? true : undefined,\n        id: formItemId,\n      })}\n    </motion.div>\n  );\n}\n\nfunction cloneChildWithA11y(\n  children: React.ReactNode,\n  a11yProps: Record<string, unknown>\n): React.ReactNode {\n  const child = Array.isArray(children) ? children[0] : children;\n  if (child && typeof child === \"object\" && \"type\" in child) {\n    const element = child as React.ReactElement<Record<string, unknown>>;\n    // biome-ignore lint/suspicious/noExplicitAny: cloneElement requires flexible typing\n    return cloneElement(element as any, a11yProps);\n  }\n  return children;\n}\n\n// ---------------------------------------------------------------------------\n// FormDescription\n// ---------------------------------------------------------------------------\n\nexport function FormDescription({\n  className,\n  children,\n  ...props\n}: FormDescriptionProps) {\n  const { formDescriptionId } = useFormFieldCtx();\n\n  return (\n    <p\n      className={cn(\"text-muted-foreground text-sm\", className)}\n      data-slot=\"form-description\"\n      id={formDescriptionId}\n      {...props}\n    >\n      {children}\n    </p>\n  );\n}\n\n// ---------------------------------------------------------------------------\n// FormMessage — animated error message with success state\n// ---------------------------------------------------------------------------\n\nexport function FormMessage({ className, children }: FormMessageProps) {\n  const shouldReduceMotion = useReducedMotion();\n  const { error, formMessageId, submitCount, prevError } = useFormFieldCtx();\n\n  const body = children ?? error;\n\n  // Show success checkmark when error was just cleared after a submit\n  const wasError = prevError && !error && submitCount > 0;\n\n  return (\n    <div>\n      <AnimatePresence mode=\"wait\">\n        {body ? (\n          <motion.p\n            animate={shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }}\n            className={cn(\"text-destructive text-sm\", className)}\n            data-slot=\"form-message\"\n            exit={\n              shouldReduceMotion\n                ? { opacity: 0, transition: { duration: 0 } }\n                : { opacity: 0, y: -4 }\n            }\n            id={formMessageId}\n            initial={\n              shouldReduceMotion ? { opacity: 1 } : { opacity: 0, y: -4 }\n            }\n            key={typeof body === \"string\" ? body : \"message\"}\n            role=\"alert\"\n            transition={shouldReduceMotion ? DURATION_INSTANT : SPRING_DEFAULT}\n          >\n            {body}\n          </motion.p>\n        ) : wasError ? (\n          <motion.div\n            animate={\n              shouldReduceMotion ? { opacity: 1 } : { opacity: 1, scale: 1 }\n            }\n            className=\"flex items-center gap-1 text-sm\"\n            exit={\n              shouldReduceMotion\n                ? { opacity: 0, transition: { duration: 0 } }\n                : { opacity: 0, scale: 0.9 }\n            }\n            initial={\n              shouldReduceMotion ? { opacity: 1 } : { opacity: 0, scale: 0.8 }\n            }\n            key=\"success\"\n            transition={\n              shouldReduceMotion\n                ? DURATION_INSTANT\n                : {\n                    damping: 20,\n                    duration: 0.25,\n                    stiffness: 300,\n                    type: \"spring\" as const,\n                  }\n            }\n          >\n            <motion.span\n              animate={shouldReduceMotion ? {} : { scale: 1 }}\n              initial={shouldReduceMotion ? {} : { scale: 0 }}\n              transition={\n                shouldReduceMotion\n                  ? DURATION_INSTANT\n                  : {\n                      damping: 15,\n                      delay: 0.05,\n                      duration: 0.2,\n                      stiffness: 400,\n                      type: \"spring\" as const,\n                    }\n              }\n            >\n              <Check className=\"size-3.5 text-emerald-500\" />\n            </motion.span>\n            <span className=\"text-emerald-500\">Looks good</span>\n          </motion.div>\n        ) : null}\n      </AnimatePresence>\n    </div>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/form/index.tsx","type":"registry:ui"}],"name":"form","registryDependencies":["https://smoothui.dev/r/lib.json"],"title":"Form","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion","lucide-react"],"description":"A GitHubStarsAnimation component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { Star } from \"lucide-react\";\nimport { motion, useReducedMotion, useSpring } from \"motion/react\";\nimport { useEffect, useState } from \"react\";\n\nconst TRANSITION_DURATION = 0.3;\nconst EASE_OUT_CUBIC = [0.215, 0.61, 0.355, 1] as const;\nconst COUNTDOWN_DURATION = 2000;\nconst AVATAR_COUNT = 5;\nconst STAGGER_DELAY = 0.05;\n\nexport interface Stargazer {\n  avatar_url: string;\n  html_url: string;\n  login: string;\n}\n\nexport interface GitHubStarsAnimationProps {\n  apiEndpoint?: string;\n  avatarClassName?: string;\n  className?: string;\n  countClassName?: string;\n  maxAvatars?: number;\n  owner?: string;\n  repo?: string;\n  showAvatars?: boolean;\n  starCount?: number;\n  stargazers?: Stargazer[];\n}\n\nexport default function GitHubStarsAnimation({\n  owner = \"educlopez\",\n  repo = \"smoothui\",\n  stargazers: providedStargazers,\n  starCount: providedStarCount,\n  apiEndpoint,\n  className = \"\",\n  avatarClassName = \"\",\n  countClassName = \"\",\n  showAvatars = true,\n  maxAvatars = AVATAR_COUNT,\n}: GitHubStarsAnimationProps) {\n  const [stargazers, setStargazers] = useState<Stargazer[]>(\n    providedStargazers || []\n  );\n  const [starCount, setStarCount] = useState(providedStarCount || 0);\n  const [displayCount, setDisplayCount] = useState(0);\n  const [isLoading, setIsLoading] = useState(!providedStargazers);\n  const [error, setError] = useState(false);\n  const shouldReduceMotion = useReducedMotion();\n\n  const countSpring = useSpring(0, {\n    damping: 30,\n    stiffness: 100,\n  });\n\n  // Fetch stargazers and star count\n  useEffect(() => {\n    if (providedStargazers && providedStarCount !== undefined) {\n      setStargazers(providedStargazers);\n      setStarCount(providedStarCount);\n      setIsLoading(false);\n      return;\n    }\n\n    const fetchData = async () => {\n      try {\n        setIsLoading(true);\n        setError(false);\n\n        // Try to fetch from custom API endpoint first\n        if (apiEndpoint) {\n          const response = await fetch(\n            `${apiEndpoint}?owner=${owner}&repo=${repo}`\n          );\n          if (response.ok) {\n            const data = await response.json();\n            if (data.stargazers) {\n              setStargazers(data.stargazers.slice(0, maxAvatars));\n            }\n            if (data.stars !== undefined) {\n              setStarCount(data.stars);\n            }\n            setIsLoading(false);\n            return;\n          }\n        }\n\n        // Fallback to GitHub API directly (client-side)\n        // Note: This has rate limits, so using a token is recommended\n        const headers: HeadersInit = {\n          Accept: \"application/vnd.github.v3+json\",\n        };\n\n        // Parallelize independent fetches to eliminate waterfall\n        const [repoResponse, stargazersResponse] = await Promise.all([\n          fetch(`https://api.github.com/repos/${owner}/${repo}`, { headers }),\n          fetch(\n            `https://api.github.com/repos/${owner}/${repo}/stargazers?per_page=${maxAvatars}`,\n            { headers }\n          ),\n        ]);\n\n        // Process repo info for star count\n        if (repoResponse.ok) {\n          try {\n            const repoData = await repoResponse.json();\n            setStarCount(repoData.stargazers_count || 0);\n          } catch {\n            // Silently fail for star count\n          }\n        }\n\n        // Process stargazers\n        if (stargazersResponse.ok) {\n          try {\n            const stargazersData =\n              (await stargazersResponse.json()) as Stargazer[];\n            setStargazers(stargazersData.slice(0, maxAvatars));\n          } catch {\n            // Silently fail for stargazers\n          }\n        }\n      } catch {\n        setError(true);\n      } finally {\n        setIsLoading(false);\n      }\n    };\n\n    fetchData();\n  }, [\n    owner,\n    repo,\n    apiEndpoint,\n    maxAvatars,\n    providedStargazers,\n    providedStarCount,\n  ]);\n\n  // Animate countdown\n  useEffect(() => {\n    if (starCount === 0 || shouldReduceMotion) {\n      if (shouldReduceMotion) {\n        setDisplayCount(starCount);\n        countSpring.set(starCount);\n      }\n      return;\n    }\n\n    const startTime = Date.now();\n    const startValue = 0;\n    const endValue = starCount;\n\n    const animate = () => {\n      const elapsed = Date.now() - startTime;\n      const progress = Math.min(elapsed / COUNTDOWN_DURATION, 1);\n\n      // Ease-out function\n      const eased = 1 - (1 - progress) ** 3;\n      const current = Math.floor(startValue + (endValue - startValue) * eased);\n\n      setDisplayCount(current);\n      countSpring.set(current);\n\n      if (progress < 1) {\n        requestAnimationFrame(animate);\n      } else {\n        setDisplayCount(endValue);\n        countSpring.set(endValue);\n      }\n    };\n\n    animate();\n  }, [starCount, countSpring, shouldReduceMotion]);\n\n  if (isLoading) {\n    return (\n      <div\n        className={cn(\"flex items-center gap-3 text-foreground/60\", className)}\n      >\n        <div className=\"h-10 w-10 animate-pulse rounded-full bg-foreground/20\" />\n        <div className=\"h-6 w-20 animate-pulse rounded bg-foreground/20\" />\n      </div>\n    );\n  }\n\n  if (error && starCount === 0) {\n    return null;\n  }\n\n  const visibleAvatars = stargazers.slice(0, maxAvatars);\n\n  return (\n    <div className={cn(\"flex items-center gap-3\", className)}>\n      {/* Avatars */}\n      {showAvatars && visibleAvatars.length > 0 && (\n        <div className=\"relative flex items-center\">\n          {visibleAvatars.map((stargazer, index) => (\n            <motion.a\n              animate={\n                shouldReduceMotion\n                  ? { opacity: 1 }\n                  : {\n                      opacity: 1,\n                      scale: 1,\n                      x: 0,\n                    }\n              }\n              aria-label={`${stargazer.login}'s GitHub profile`}\n              className={cn(\n                \"relative z-10 h-10 w-10 overflow-hidden rounded-full border-2 border-background bg-background transition-transform hover:z-20 hover:scale-110\",\n                avatarClassName\n              )}\n              href={stargazer.html_url}\n              initial={\n                shouldReduceMotion\n                  ? { opacity: 1 }\n                  : {\n                      opacity: 0,\n                      scale: 0.8,\n                      x: -20,\n                    }\n              }\n              key={stargazer.login}\n              rel=\"noopener noreferrer\"\n              style={{\n                marginLeft: index > 0 ? \"-8px\" : \"0\",\n              }}\n              target=\"_blank\"\n              transition={\n                shouldReduceMotion\n                  ? { duration: 0 }\n                  : {\n                      delay: index * STAGGER_DELAY,\n                      duration: TRANSITION_DURATION,\n                      ease: EASE_OUT_CUBIC,\n                    }\n              }\n              whileHover={shouldReduceMotion ? {} : { scale: 1.1, zIndex: 20 }}\n            >\n              <img\n                alt={`${stargazer.login}'s avatar`}\n                className=\"h-full w-full object-cover\"\n                draggable={false}\n                src={stargazer.avatar_url}\n              />\n            </motion.a>\n          ))}\n        </div>\n      )}\n\n      {/* Star count */}\n      <motion.div\n        animate={shouldReduceMotion ? { opacity: 1 } : { opacity: 1, scale: 1 }}\n        className={cn(\"flex items-center gap-1.5 font-medium\", countClassName)}\n        initial={\n          shouldReduceMotion ? { opacity: 1 } : { opacity: 0, scale: 0.9 }\n        }\n        transition={\n          shouldReduceMotion\n            ? { duration: 0 }\n            : {\n                duration: TRANSITION_DURATION,\n                ease: EASE_OUT_CUBIC,\n              }\n        }\n      >\n        <Star className=\"h-4 w-4 fill-current\" />\n        <motion.span\n          animate={shouldReduceMotion ? { scale: 1 } : { scale: [1, 1.1, 1] }}\n          className=\"tabular-nums\"\n          transition={\n            shouldReduceMotion\n              ? { duration: 0 }\n              : {\n                  duration: 0.3,\n                  ease: EASE_OUT_CUBIC,\n                }\n          }\n        >\n          {displayCount.toLocaleString()}\n        </motion.span>\n        <span className=\"text-foreground/70 text-sm\">\n          {displayCount === 1 ? \"star\" : \"stars\"}\n        </span>\n      </motion.div>\n    </div>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/github-stars-animation/index.tsx","type":"registry:ui"}],"name":"github-stars-animation","registryDependencies":[],"title":"Github Stars Animation","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"A GlowHoverCards component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { useReducedMotion } from \"motion/react\";\nimport {\n  type CSSProperties,\n  cloneElement,\n  type ReactElement,\n  type Ref,\n  useEffect,\n  useRef,\n  useState,\n} from \"react\";\n\nexport interface GlowHoverTheme {\n  hue: number;\n  lightness: number;\n  saturation: number;\n}\n\nexport interface GlowHoverItem {\n  element: ReactElement;\n  id: string;\n  theme?: GlowHoverTheme;\n}\n\nexport interface GlowHoverProps {\n  className?: string;\n  glowIntensity?: number;\n  items: GlowHoverItem[];\n  maskSize?: number;\n}\n\n// Legacy types for backward compatibility\nexport type GlowHoverCardTheme = GlowHoverTheme;\nexport type GlowHoverCardItem = GlowHoverItem;\nexport type GlowHoverCardsProps = GlowHoverProps;\n\nexport default function GlowHover({\n  items,\n  className = \"\",\n  maskSize = 400,\n  glowIntensity = 0.15,\n}: GlowHoverProps) {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const overlayRef = useRef<HTMLDivElement>(null);\n  const itemRefs = useRef<(HTMLElement | null)[]>([]);\n  const overlayItemRefs = useRef<(HTMLElement | null)[]>([]);\n  const [mousePosition, setMousePosition] = useState<{\n    x: number;\n    y: number;\n    opacity: number;\n  }>({ opacity: 0, x: 0, y: 0 });\n  const shouldReduceMotion = useReducedMotion();\n\n  useEffect(() => {\n    const container = containerRef.current;\n    if (!container || shouldReduceMotion) {\n      return;\n    }\n\n    const handlePointerMove = (e: PointerEvent) => {\n      const rect = container.getBoundingClientRect();\n      // Use clientX/clientY for viewport coordinates, then subtract container position\n      const x = e.clientX - rect.left;\n      const y = e.clientY - rect.top;\n\n      setMousePosition({\n        opacity: 1,\n        x,\n        y,\n      });\n    };\n\n    const handlePointerLeave = () => {\n      setMousePosition((prev) => ({ ...prev, opacity: 0 }));\n    };\n\n    container.addEventListener(\"pointermove\", handlePointerMove);\n    container.addEventListener(\"pointerleave\", handlePointerLeave);\n\n    return () => {\n      container.removeEventListener(\"pointermove\", handlePointerMove);\n      container.removeEventListener(\"pointerleave\", handlePointerLeave);\n    };\n  }, [shouldReduceMotion]);\n\n  // Sync overlay card sizes and positions with original cards\n  useEffect(() => {\n    if (shouldReduceMotion || !overlayRef.current || !containerRef.current) {\n      return;\n    }\n\n    const syncCards = () => {\n      const container = containerRef.current;\n      const overlay = overlayRef.current;\n      if (!(container && overlay)) {\n        return;\n      }\n\n      itemRefs.current.forEach((itemEl, index) => {\n        const overlayItemEl = overlayItemRefs.current[index];\n        if (!(itemEl && overlayItemEl)) {\n          return;\n        }\n\n        const itemRect = itemEl.getBoundingClientRect();\n        const containerRect = container.getBoundingClientRect();\n\n        // Calculate position relative to container\n        const left = itemRect.left - containerRect.left;\n        const top = itemRect.top - containerRect.top;\n\n        overlayItemEl.style.position = \"absolute\";\n        overlayItemEl.style.left = `${left}px`;\n        overlayItemEl.style.top = `${top}px`;\n        overlayItemEl.style.width = `${itemRect.width}px`;\n        overlayItemEl.style.height = `${itemRect.height}px`;\n      });\n    };\n\n    const observers: ResizeObserver[] = [];\n    const mutationObserver = new MutationObserver(syncCards);\n\n    // Sync on resize\n    for (const itemEl of itemRefs.current) {\n      if (!itemEl) {\n        continue;\n      }\n\n      const observer = new ResizeObserver(() => {\n        syncCards();\n      });\n\n      observer.observe(itemEl);\n      observers.push(observer);\n    }\n\n    // Sync on DOM mutations\n    if (containerRef.current) {\n      mutationObserver.observe(containerRef.current, {\n        attributes: true,\n        childList: true,\n        subtree: true,\n      });\n    }\n\n    // Initial sync\n    syncCards();\n\n    // Sync on scroll and resize\n    window.addEventListener(\"scroll\", syncCards, { passive: true });\n    window.addEventListener(\"resize\", syncCards);\n\n    return () => {\n      for (const observer of observers) {\n        observer.disconnect();\n      }\n      mutationObserver.disconnect();\n      window.removeEventListener(\"scroll\", syncCards);\n      window.removeEventListener(\"resize\", syncCards);\n    };\n  }, [shouldReduceMotion]);\n\n  // Apply glow effect styles to an element\n  const applyGlowStyles = (\n    element: ReactElement,\n    theme?: GlowHoverTheme,\n    isOverlay = false\n  ): ReactElement => {\n    if (!isOverlay) {\n      return element;\n    }\n\n    const props = element.props as {\n      style?: CSSProperties;\n      className?: string;\n    };\n    const existingStyle = props.style || {};\n    const existingClassName = props.className || \"\";\n\n    let glowStyles: CSSProperties;\n\n    if (theme) {\n      // Use theme HSL colors\n      const hsl = `${theme.hue}, ${theme.saturation}%, ${theme.lightness}%`;\n      glowStyles = {\n        backgroundColor: `hsla(${hsl}, ${glowIntensity})`,\n        borderColor: `hsla(${hsl}, 1)`,\n        boxShadow: `0 0 0 1px inset hsl(${hsl}), 0 0 20px hsla(${hsl}, ${glowIntensity})`,\n      };\n    } else {\n      // Use brand color from CSS variable (OKLCH format supports / opacity)\n      const brandColor = \"var(--color-brand)\";\n      // OKLCH format: oklch(L C H / opacity)\n      const brandWithOpacity = `color-mix(in oklch, ${brandColor}, transparent ${(1 - glowIntensity) * 100}%)`;\n      glowStyles = {\n        backgroundColor: brandWithOpacity,\n        borderColor: brandColor,\n        boxShadow: `0 0 0 1px inset ${brandColor}, 0 0 20px ${brandWithOpacity}`,\n      };\n    }\n\n    // Merge with existing styles\n    const mergedStyle = {\n      ...existingStyle,\n      ...glowStyles,\n    };\n\n    return cloneElement(element, {\n      ...props,\n      className: cn(existingClassName, \"glow-overlay-item\"),\n      style: mergedStyle,\n      // biome-ignore lint/suspicious/noExplicitAny: cloneElement requires flexible typing\n    } as any);\n  };\n\n  return (\n    <div\n      className={cn(\"relative\", className)}\n      ref={containerRef}\n      style={shouldReduceMotion ? undefined : { willChange: \"contents\" }}\n    >\n      {/* Original Items */}\n      <div className=\"contents\">\n        {items.map((item, index) =>\n          cloneElement(item.element, {\n            key: item.id,\n            ref: (el: HTMLElement | null) => {\n              itemRefs.current[index] = el;\n              // Preserve existing ref if any\n              const elementProps = item.element.props as {\n                ref?: Ref<HTMLElement>;\n              };\n              const existingRef = elementProps?.ref;\n              if (typeof existingRef === \"function\") {\n                existingRef(el);\n              } else if (existingRef && typeof existingRef === \"object\") {\n                (existingRef as { current: HTMLElement | null }).current = el;\n              }\n            },\n            // biome-ignore lint/suspicious/noExplicitAny: cloneElement requires flexible typing\n          } as any)\n        )}\n      </div>\n\n      {/* Overlay with Glow Effect */}\n      {!shouldReduceMotion && (\n        <div\n          aria-hidden=\"true\"\n          className=\"pointer-events-none absolute inset-0 select-none\"\n          ref={overlayRef}\n          style={{\n            maskImage: `radial-gradient(${maskSize}px ${maskSize}px at ${mousePosition.x}px ${mousePosition.y}px, #000 1%, transparent 50%)`,\n            opacity: mousePosition.opacity,\n            transition:\n              \"opacity 200ms ease, mask-image 200ms ease, -webkit-mask-image 200ms ease\",\n            WebkitMaskImage: `radial-gradient(${maskSize}px ${maskSize}px at ${mousePosition.x}px ${mousePosition.y}px, #000 1%, transparent 50%)`,\n            willChange: \"mask-image, opacity\",\n          }}\n        >\n          {items.map((item, index) => {\n            const glowElement = applyGlowStyles(item.element, item.theme, true);\n            return cloneElement(glowElement, {\n              key: item.id,\n              ref: (el: HTMLElement | null) => {\n                overlayItemRefs.current[index] = el;\n              },\n              // biome-ignore lint/suspicious/noExplicitAny: cloneElement requires flexible typing\n            } as any);\n          })}\n        </div>\n      )}\n    </div>\n  );\n}\n\n// Legacy export for backward compatibility\nexport function GlowHoverCards(props: GlowHoverCardsProps) {\n  return <GlowHover {...props} />;\n}\n","path":"index.tsx","target":"components/smoothui/glow-hover-card/index.tsx","type":"registry:ui"}],"name":"glow-hover-card","registryDependencies":["https://smoothui.dev/r/tokens.json"],"title":"Glow Hover Card","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["gsap"],"description":"A GooeyPopover component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport gsap from \"gsap\";\nimport { useCallback, useEffect, useId, useRef, useState } from \"react\";\nimport { useClickOutside } from \"./use-click-outside\";\n\nconst MEASURE_DELAY_SHORT = 100;\nconst MEASURE_DELAY_LONG = 500;\nconst DEFAULT_TRIGGER_SIZE = 44;\nconst DEFAULT_CONTENT_WIDTH = 240;\nconst DEFAULT_SIDE_OFFSET = 24;\nconst DEFAULT_SPEED = 0.25;\nconst GOO_STD_DEVIATION = 10;\nconst GOO_MATRIX_ALPHA_MULTIPLIER = 24;\nconst GOO_MATRIX_ALPHA_OFFSET = -10;\nconst CONTENT_BORDER_RADIUS = 18;\n\nexport type GooeyPopoverProps = {\n  children: React.ReactNode;\n  trigger?: React.ReactNode;\n  triggerSize?: number;\n  isOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  side?: \"top\" | \"bottom\";\n  sideOffset?: number;\n  contentWidth?: number;\n  speed?: number;\n  bgClassName?: string;\n  contentClassName?: string;\n  className?: string;\n};\n\nexport default function GooeyPopover({\n  children,\n  trigger,\n  triggerSize = DEFAULT_TRIGGER_SIZE,\n  isOpen: controlledIsOpen,\n  onOpenChange,\n  side = \"top\",\n  sideOffset = DEFAULT_SIDE_OFFSET,\n  contentWidth = DEFAULT_CONTENT_WIDTH,\n  speed = DEFAULT_SPEED,\n  bgClassName = \"bg-neutral-900\",\n  contentClassName,\n  className,\n}: GooeyPopoverProps) {\n  const filterId = useId();\n  const isControlled = controlledIsOpen !== undefined;\n  const [internalIsOpen, setInternalIsOpen] = useState(false);\n  const isOpen = isControlled ? controlledIsOpen : internalIsOpen;\n  const [isVisible, setIsVisible] = useState(false);\n\n  const containerRef = useRef<HTMLDivElement>(null);\n  const measureRef = useRef<HTMLDivElement>(null);\n  const filteredContentRef = useRef<HTMLDivElement>(null);\n  const unfilteredContentRef = useRef<HTMLDivElement>(null);\n  const innerContentRef = useRef<HTMLDivElement>(null);\n  const timelineRef = useRef<gsap.core.Timeline | null>(null);\n  const [contentHeight, setContentHeight] = useState(0);\n  const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);\n\n  useEffect(() => {\n    const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    setPrefersReducedMotion(mq.matches);\n    const handler = (e: MediaQueryListEvent) => {\n      setPrefersReducedMotion(e.matches);\n    };\n    mq.addEventListener(\"change\", handler);\n    return () => mq.removeEventListener(\"change\", handler);\n  }, []);\n\n  const setIsOpen = useCallback(\n    (open: boolean) => {\n      if (!isControlled) {\n        setInternalIsOpen(open);\n      }\n      onOpenChange?.(open);\n    },\n    [isControlled, onOpenChange]\n  );\n\n  const handleClose = useCallback(() => {\n    if (isOpen) {\n      setIsOpen(false);\n    }\n  }, [isOpen, setIsOpen]);\n\n  useClickOutside(containerRef, handleClose);\n\n  // Escape key to close\n  useEffect(() => {\n    const handleKeyDown = (event: KeyboardEvent) => {\n      if (event.key === \"Escape\" && isOpen) {\n        setIsOpen(false);\n      }\n    };\n    window.addEventListener(\"keydown\", handleKeyDown);\n    return () => window.removeEventListener(\"keydown\", handleKeyDown);\n  }, [isOpen, setIsOpen]);\n\n  // Measure content height\n  useEffect(() => {\n    const measureHeight = () => {\n      if (measureRef.current) {\n        const height = measureRef.current.scrollHeight;\n        if (height > 0) {\n          setContentHeight(height);\n        }\n      }\n    };\n\n    const timeoutId = setTimeout(measureHeight, MEASURE_DELAY_SHORT);\n    const timeoutId2 = setTimeout(measureHeight, MEASURE_DELAY_LONG);\n    return () => {\n      clearTimeout(timeoutId);\n      clearTimeout(timeoutId2);\n    };\n  }, [children]);\n\n  const triggerRadius = triggerSize / 2;\n  // Position content so its edge clears the trigger with sideOffset gap\n  const translateY =\n    side === \"top\" ? -(contentHeight + sideOffset) : triggerSize + sideOffset;\n  const contentLeft = triggerRadius - contentWidth / 2;\n\n  // GSAP animations\n  useEffect(() => {\n    if (contentHeight === 0) {\n      return;\n    }\n\n    // Kill any running timeline\n    if (timelineRef.current) {\n      timelineRef.current.kill();\n    }\n\n    const filteredTarget = filteredContentRef.current;\n    const unfilteredTarget = unfilteredContentRef.current;\n    const innerTarget = innerContentRef.current;\n\n    if (!(unfilteredTarget && innerTarget)) {\n      return;\n    }\n\n    if (prefersReducedMotion) {\n      if (isOpen) {\n        setIsVisible(true);\n        gsap.set(unfilteredTarget, {\n          borderRadius: CONTENT_BORDER_RADIUS,\n          height: contentHeight,\n          opacity: 1,\n          width: contentWidth,\n          x: contentLeft,\n          y: translateY,\n        });\n        gsap.set(innerTarget, { opacity: 1, y: 0 });\n      } else {\n        gsap.set(unfilteredTarget, {\n          borderRadius: triggerRadius,\n          height: triggerSize,\n          opacity: 0,\n          width: triggerSize,\n          x: 0,\n          y: 0,\n        });\n        gsap.set(innerTarget, { opacity: 0, y: 0 });\n        setIsVisible(false);\n      }\n      return;\n    }\n\n    if (isOpen) {\n      setIsVisible(true);\n\n      // Start both content shapes as circles at trigger position\n      const startProps = {\n        borderRadius: triggerRadius,\n        height: triggerSize,\n        opacity: 1,\n        width: triggerSize,\n        x: 0,\n        y: 0,\n      };\n      if (filteredTarget) {\n        gsap.set(filteredTarget, startProps);\n      }\n      gsap.set(unfilteredTarget, startProps);\n      gsap.set(innerTarget, { opacity: 0, y: 16 });\n\n      const tl = gsap.timeline();\n\n      // Filtered content: morph to rectangle with borderRadius=0\n      // The goo filter softens the edges naturally\n      if (filteredTarget) {\n        tl.to(\n          filteredTarget,\n          {\n            borderRadius: 0,\n            duration: speed,\n            ease: \"power1.in\",\n            height: contentHeight,\n            width: contentWidth,\n            x: contentLeft,\n            y: translateY,\n          },\n          0\n        );\n      }\n\n      // Unfiltered content: morph to rectangle with rounded corners\n      tl.to(\n        unfilteredTarget,\n        {\n          borderRadius: CONTENT_BORDER_RADIUS,\n          duration: speed,\n          ease: \"power1.in\",\n          height: contentHeight,\n          width: contentWidth,\n          x: contentLeft,\n          y: translateY,\n        },\n        0\n      );\n\n      // Content text fade in (overlapping with shape morph)\n      tl.to(\n        innerTarget,\n        {\n          duration: speed * 0.75,\n          ease: \"power1.out\",\n          opacity: 1,\n          y: 0,\n        },\n        speed * 0.575\n      );\n\n      timelineRef.current = tl;\n    } else {\n      // Content text fade out first\n      const tl = gsap.timeline({\n        onComplete: () => {\n          setIsVisible(false);\n        },\n      });\n\n      tl.to(innerTarget, {\n        duration: speed * 0.4,\n        ease: \"power1.in\",\n        opacity: 0,\n        y: 8,\n      });\n\n      // Shape morph back: rectangle → circle\n      const targets = [filteredTarget, unfilteredTarget].filter(Boolean);\n      tl.to(\n        targets,\n        {\n          borderRadius: triggerRadius,\n          duration: speed,\n          ease: \"power1.in\",\n          height: triggerSize,\n          width: triggerSize,\n          x: 0,\n          y: 0,\n        },\n        speed * 0.2\n      );\n\n      // Fade out at the end\n      tl.to(\n        targets,\n        {\n          duration: speed * 0.3,\n          ease: \"power1.in\",\n          opacity: 0,\n        },\n        `-=${speed * 0.3}`\n      );\n\n      timelineRef.current = tl;\n    }\n\n    return () => {\n      if (timelineRef.current) {\n        timelineRef.current.kill();\n      }\n    };\n  }, [\n    isOpen,\n    contentHeight,\n    contentWidth,\n    triggerSize,\n    triggerRadius,\n    contentLeft,\n    translateY,\n    speed,\n    prefersReducedMotion,\n  ]);\n\n  const defaultTriggerIcon = (\n    <svg\n      fill=\"none\"\n      height={20}\n      stroke=\"currentColor\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      strokeWidth={2}\n      viewBox=\"0 0 24 24\"\n      width={20}\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <line x1=\"12\" x2=\"12\" y1=\"5\" y2=\"19\" />\n      <line x1=\"5\" x2=\"19\" y1=\"12\" y2=\"12\" />\n    </svg>\n  );\n\n  return (\n    <div className={cn(\"relative inline-flex\", className)} ref={containerRef}>\n      {/* SVG goo filter definition */}\n      <svg\n        aria-hidden=\"true\"\n        className=\"absolute\"\n        style={{ height: 0, width: 0 }}\n      >\n        <defs>\n          <filter id={filterId}>\n            <feGaussianBlur\n              in=\"SourceGraphic\"\n              result=\"blur\"\n              stdDeviation={GOO_STD_DEVIATION}\n            />\n            <feColorMatrix\n              in=\"blur\"\n              result=\"goo\"\n              type=\"matrix\"\n              values={`1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 ${String(GOO_MATRIX_ALPHA_MULTIPLIER)} ${String(GOO_MATRIX_ALPHA_OFFSET)}`}\n            />\n            <feComposite in=\"SourceGraphic\" in2=\"goo\" operator=\"atop\" />\n          </filter>\n        </defs>\n      </svg>\n\n      {/* Hidden measurement div */}\n      <div\n        aria-hidden=\"true\"\n        className=\"pointer-events-none absolute\"\n        ref={measureRef}\n        style={{\n          left: -9999,\n          position: \"absolute\",\n          top: -9999,\n          visibility: \"hidden\",\n          width: contentWidth,\n        }}\n      >\n        <div className={cn(\"p-4\", contentClassName)}>{children}</div>\n      </div>\n\n      {/* Filtered layer: SVG goo filter creates liquid bridge between blob and content */}\n      {!prefersReducedMotion && (isOpen || isVisible) && (\n        <div\n          aria-hidden=\"true\"\n          className=\"pointer-events-none absolute inset-0\"\n          style={{ filter: `url(#${filterId})` }}\n        >\n          {/* Blob: circle at trigger position — stays put while content morphs away */}\n          <div\n            className={cn(\"absolute rounded-full\", bgClassName)}\n            style={{\n              height: triggerSize,\n              left: 0,\n              top: 0,\n              width: triggerSize,\n            }}\n          />\n\n          {/* Content shape: morphs from circle to rectangle (borderRadius=0, goo softens edges) */}\n          <div\n            className={cn(\"absolute\", bgClassName)}\n            ref={filteredContentRef}\n            style={{\n              borderRadius: triggerRadius,\n              height: triggerSize,\n              left: 0,\n              opacity: 0,\n              top: 0,\n              width: triggerSize,\n            }}\n          />\n        </div>\n      )}\n\n      {/* Trigger button (z-10, sits on top of filtered blob) */}\n      <button\n        aria-expanded={isOpen}\n        aria-haspopup=\"dialog\"\n        className={cn(\n          \"relative z-10 flex cursor-pointer items-center justify-center rounded-full text-white transition-colors\",\n          bgClassName\n        )}\n        onClick={() => setIsOpen(!isOpen)}\n        style={{\n          height: triggerSize,\n          width: triggerSize,\n        }}\n        type=\"button\"\n      >\n        {trigger ?? defaultTriggerIcon}\n      </button>\n\n      {/* Unfiltered content panel (z-10, clean edges, actual text) */}\n      {isOpen || isVisible ? (\n        <div\n          className={cn(\n            \"absolute z-10 overflow-hidden text-white\",\n            bgClassName\n          )}\n          ref={unfilteredContentRef}\n          role=\"dialog\"\n          style={{\n            borderRadius: triggerRadius,\n            height: triggerSize,\n            left: 0,\n            opacity: 0,\n            top: 0,\n            width: triggerSize,\n          }}\n        >\n          <div\n            className={cn(\"p-4\", contentClassName)}\n            ref={innerContentRef}\n            style={{ opacity: 0, transform: \"translateY(16px)\" }}\n          >\n            {children}\n          </div>\n        </div>\n      ) : null}\n    </div>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/gooey-popover/index.tsx","type":"registry:ui"},{"content":"\"use client\";\n\nimport { type RefObject, useEffect } from \"react\";\n\ntype EventType =\n  | \"mousedown\"\n  | \"mouseup\"\n  | \"touchstart\"\n  | \"touchend\"\n  | \"focusin\"\n  | \"focusout\";\n\nexport function useClickOutside<T extends HTMLElement = HTMLElement>(\n  ref: RefObject<T | null> | RefObject<T | null>[],\n  handler: (event: Event) => void,\n  eventType: EventType = \"mousedown\"\n): void {\n  useEffect(() => {\n    function callback(event: Event) {\n      const target = event.target as Node;\n\n      // Do nothing if the target is not connected element with document\n      if (!target?.isConnected) {\n        return;\n      }\n\n      const isOutside = Array.isArray(ref)\n        ? ref\n            .filter((r) => Boolean(r.current))\n            .every((r) => r.current && !r.current.contains(target))\n        : ref.current && !ref.current.contains(target);\n\n      if (isOutside) {\n        handler(event);\n      }\n    }\n\n    window.addEventListener(eventType, callback);\n\n    return () => {\n      window.removeEventListener(eventType, callback);\n    };\n  }, [ref, handler, eventType]);\n}\n","path":"use-click-outside.tsx","target":"components/smoothui/gooey-popover/use-click-outside.tsx","type":"registry:ui"}],"name":"gooey-popover","registryDependencies":[],"title":"Gooey Popover","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"3x3 grid-based loading animation with preset patterns","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { useEffect, useMemo, useState } from \"react\";\n\n// Grid matrix type: 3x3 array of 0s and 1s\nexport type GridMatrix = [\n  [0 | 1, 0 | 1, 0 | 1],\n  [0 | 1, 0 | 1, 0 | 1],\n  [0 | 1, 0 | 1, 0 | 1],\n];\n\n// All preset pattern names\nexport type PresetPattern =\n  // Solo (single cell)\n  | \"solo-center\"\n  | \"solo-tl\"\n  | \"solo-tr\"\n  | \"solo-bl\"\n  | \"solo-br\"\n  // Horizontal lines\n  | \"line-h-top\"\n  | \"line-h-mid\"\n  | \"line-h-bot\"\n  // Vertical lines\n  | \"line-v-left\"\n  | \"line-v-mid\"\n  | \"line-v-right\"\n  // Diagonals\n  | \"line-diag-1\"\n  | \"line-diag-2\"\n  // Corners\n  | \"corners-only\"\n  | \"corners-sync\"\n  | \"corners\"\n  // Plus\n  | \"plus-hollow\"\n  | \"plus-full\"\n  // L-shapes\n  | \"L-tl\"\n  | \"L-tr\"\n  | \"L-bl\"\n  | \"L-br\"\n  // T-shapes\n  | \"T-top\"\n  | \"T-bot\"\n  | \"T-left\"\n  | \"T-right\"\n  // Duo\n  | \"duo-h\"\n  | \"duo-v\"\n  | \"duo-diag\"\n  // Frame\n  | \"frame\"\n  | \"frame-sync\"\n  // Sparse\n  | \"sparse-1\"\n  | \"sparse-2\"\n  | \"sparse-3\"\n  // Waves\n  | \"wave-lr\"\n  | \"wave-rl\"\n  | \"wave-tb\"\n  | \"wave-bt\"\n  // Diagonal quadrants\n  | \"diagonal-tl\"\n  | \"diagonal-tr\"\n  | \"diagonal-bl\"\n  | \"diagonal-br\"\n  // Ripple\n  | \"ripple-out\"\n  | \"ripple-in\"\n  // Shapes\n  | \"cross\"\n  | \"x-shape\"\n  | \"diamond\"\n  // Stripes\n  | \"stripes-h\"\n  | \"stripes-v\"\n  // Patterns\n  | \"checkerboard\"\n  | \"rows-alt\"\n  // Spirals\n  | \"spiral-cw\"\n  | \"spiral-ccw\"\n  // Snake\n  | \"snake\"\n  | \"snake-rev\"\n  // Rain\n  | \"rain\"\n  | \"rain-rev\"\n  // Effects\n  | \"waterfall\"\n  | \"breathing\"\n  | \"heartbeat\"\n  | \"twinkle\"\n  | \"sparkle\"\n  | \"chaos\"\n  // Edge\n  | \"edge-cw\"\n  | \"border\";\n\nexport interface GridLoaderProps {\n  /** Blur amount in pixels — creates a soft glow effect */\n  blur?: number;\n  /** Additional CSS classes */\n  className?: string;\n  /** Color preset or custom CSS color */\n  color?: \"white\" | \"red\" | \"blue\" | \"green\" | \"amber\" | string;\n  /** Gap between cells in pixels */\n  gap?: number;\n  /** Animation mode */\n  mode?: \"pulse\" | \"sequence\" | \"stagger\";\n  /** Pattern to display - preset name or custom 3x3 matrix */\n  pattern?: PresetPattern | GridMatrix;\n  /** Use rounded (circular) cells instead of square */\n  rounded?: boolean;\n  /** Array of patterns for sequence mode */\n  sequence?: Array<PresetPattern | GridMatrix>;\n  /** Size preset or pixel value */\n  size?: \"sm\" | \"md\" | \"lg\" | \"xl\" | number;\n  /** Animation speed */\n  speed?: \"slow\" | \"normal\" | \"fast\";\n  /** Disable animation and show static pattern */\n  static?: boolean;\n}\n\n// Preset patterns as 3x3 matrices\nconst PATTERNS: Record<PresetPattern, GridMatrix> = {\n  border: [\n    [1, 1, 1],\n    [1, 0, 1],\n    [1, 1, 1],\n  ],\n  breathing: [\n    [0, 0, 0],\n    [0, 1, 0],\n    [0, 0, 0],\n  ],\n  chaos: [\n    [1, 0, 1],\n    [0, 1, 0],\n    [1, 0, 0],\n  ],\n\n  // Patterns\n  checkerboard: [\n    [1, 0, 1],\n    [0, 1, 0],\n    [1, 0, 1],\n  ],\n  corners: [\n    [1, 0, 1],\n    [0, 0, 0],\n    [1, 0, 1],\n  ],\n\n  // Corners\n  \"corners-only\": [\n    [1, 0, 1],\n    [0, 0, 0],\n    [1, 0, 1],\n  ],\n  \"corners-sync\": [\n    [1, 0, 1],\n    [0, 0, 0],\n    [1, 0, 1],\n  ],\n\n  // Shapes\n  cross: [\n    [0, 1, 0],\n    [1, 1, 1],\n    [0, 1, 0],\n  ],\n  \"diagonal-bl\": [\n    [0, 0, 0],\n    [1, 1, 0],\n    [1, 1, 0],\n  ],\n  \"diagonal-br\": [\n    [0, 0, 0],\n    [0, 1, 1],\n    [0, 1, 1],\n  ],\n\n  // Diagonal quadrants\n  \"diagonal-tl\": [\n    [1, 1, 0],\n    [1, 1, 0],\n    [0, 0, 0],\n  ],\n  \"diagonal-tr\": [\n    [0, 1, 1],\n    [0, 1, 1],\n    [0, 0, 0],\n  ],\n  diamond: [\n    [0, 1, 0],\n    [1, 0, 1],\n    [0, 1, 0],\n  ],\n  \"duo-diag\": [\n    [1, 0, 0],\n    [0, 1, 0],\n    [0, 0, 0],\n  ],\n\n  // Duo (two cells)\n  \"duo-h\": [\n    [0, 0, 0],\n    [1, 1, 0],\n    [0, 0, 0],\n  ],\n  \"duo-v\": [\n    [0, 1, 0],\n    [0, 1, 0],\n    [0, 0, 0],\n  ],\n\n  // Edge\n  \"edge-cw\": [\n    [1, 1, 1],\n    [0, 0, 0],\n    [0, 0, 0],\n  ],\n\n  // Frame\n  frame: [\n    [1, 1, 1],\n    [1, 0, 1],\n    [1, 1, 1],\n  ],\n  \"frame-sync\": [\n    [1, 1, 1],\n    [1, 0, 1],\n    [1, 1, 1],\n  ],\n  heartbeat: [\n    [0, 1, 0],\n    [1, 1, 1],\n    [0, 1, 0],\n  ],\n  \"L-bl\": [\n    [0, 0, 0],\n    [1, 0, 0],\n    [1, 1, 0],\n  ],\n  \"L-br\": [\n    [0, 0, 0],\n    [0, 0, 1],\n    [0, 1, 1],\n  ],\n\n  // L-shapes\n  \"L-tl\": [\n    [1, 1, 0],\n    [1, 0, 0],\n    [0, 0, 0],\n  ],\n  \"L-tr\": [\n    [0, 1, 1],\n    [0, 0, 1],\n    [0, 0, 0],\n  ],\n\n  // Diagonals\n  \"line-diag-1\": [\n    [1, 0, 0],\n    [0, 1, 0],\n    [0, 0, 1],\n  ],\n  \"line-diag-2\": [\n    [0, 0, 1],\n    [0, 1, 0],\n    [1, 0, 0],\n  ],\n  \"line-h-bot\": [\n    [0, 0, 0],\n    [0, 0, 0],\n    [1, 1, 1],\n  ],\n  \"line-h-mid\": [\n    [0, 0, 0],\n    [1, 1, 1],\n    [0, 0, 0],\n  ],\n\n  // Horizontal lines\n  \"line-h-top\": [\n    [1, 1, 1],\n    [0, 0, 0],\n    [0, 0, 0],\n  ],\n\n  // Vertical lines\n  \"line-v-left\": [\n    [1, 0, 0],\n    [1, 0, 0],\n    [1, 0, 0],\n  ],\n  \"line-v-mid\": [\n    [0, 1, 0],\n    [0, 1, 0],\n    [0, 1, 0],\n  ],\n  \"line-v-right\": [\n    [0, 0, 1],\n    [0, 0, 1],\n    [0, 0, 1],\n  ],\n  \"plus-full\": [\n    [0, 1, 0],\n    [1, 1, 1],\n    [0, 1, 0],\n  ],\n\n  // Plus\n  \"plus-hollow\": [\n    [0, 1, 0],\n    [1, 0, 1],\n    [0, 1, 0],\n  ],\n\n  // Rain\n  rain: [\n    [0, 1, 0],\n    [0, 1, 0],\n    [0, 0, 0],\n  ],\n  \"rain-rev\": [\n    [0, 0, 0],\n    [0, 1, 0],\n    [0, 1, 0],\n  ],\n  \"ripple-in\": [\n    [0, 0, 0],\n    [0, 1, 0],\n    [0, 0, 0],\n  ],\n\n  // Ripple\n  \"ripple-out\": [\n    [1, 1, 1],\n    [1, 0, 1],\n    [1, 1, 1],\n  ],\n  \"rows-alt\": [\n    [1, 1, 1],\n    [0, 0, 0],\n    [1, 1, 1],\n  ],\n\n  // Snake\n  snake: [\n    [1, 1, 0],\n    [0, 1, 0],\n    [0, 0, 0],\n  ],\n  \"snake-rev\": [\n    [0, 0, 0],\n    [0, 1, 0],\n    [0, 1, 1],\n  ],\n  \"solo-bl\": [\n    [0, 0, 0],\n    [0, 0, 0],\n    [1, 0, 0],\n  ],\n  \"solo-br\": [\n    [0, 0, 0],\n    [0, 0, 0],\n    [0, 0, 1],\n  ],\n  // Solo (single cell)\n  \"solo-center\": [\n    [0, 0, 0],\n    [0, 1, 0],\n    [0, 0, 0],\n  ],\n  \"solo-tl\": [\n    [1, 0, 0],\n    [0, 0, 0],\n    [0, 0, 0],\n  ],\n  \"solo-tr\": [\n    [0, 0, 1],\n    [0, 0, 0],\n    [0, 0, 0],\n  ],\n  sparkle: [\n    [1, 0, 1],\n    [0, 1, 0],\n    [1, 0, 1],\n  ],\n\n  // Sparse\n  \"sparse-1\": [\n    [1, 0, 0],\n    [0, 0, 1],\n    [0, 1, 0],\n  ],\n  \"sparse-2\": [\n    [0, 1, 0],\n    [1, 0, 0],\n    [0, 0, 1],\n  ],\n  \"sparse-3\": [\n    [0, 0, 1],\n    [0, 1, 0],\n    [1, 0, 0],\n  ],\n  \"spiral-ccw\": [\n    [1, 1, 1],\n    [1, 0, 0],\n    [1, 0, 0],\n  ],\n\n  // Spirals\n  \"spiral-cw\": [\n    [1, 1, 1],\n    [0, 0, 1],\n    [0, 0, 1],\n  ],\n\n  // Stripes\n  \"stripes-h\": [\n    [1, 1, 1],\n    [0, 0, 0],\n    [1, 1, 1],\n  ],\n  \"stripes-v\": [\n    [1, 0, 1],\n    [1, 0, 1],\n    [1, 0, 1],\n  ],\n  \"T-bot\": [\n    [0, 0, 0],\n    [0, 1, 0],\n    [1, 1, 1],\n  ],\n  \"T-left\": [\n    [1, 0, 0],\n    [1, 1, 0],\n    [1, 0, 0],\n  ],\n  \"T-right\": [\n    [0, 0, 1],\n    [0, 1, 1],\n    [0, 0, 1],\n  ],\n\n  // T-shapes\n  \"T-top\": [\n    [1, 1, 1],\n    [0, 1, 0],\n    [0, 0, 0],\n  ],\n  twinkle: [\n    [1, 0, 0],\n    [0, 0, 1],\n    [0, 1, 0],\n  ],\n\n  // Effects\n  waterfall: [\n    [1, 1, 0],\n    [0, 0, 0],\n    [0, 0, 0],\n  ],\n  \"wave-bt\": [\n    [0, 0, 0],\n    [1, 1, 1],\n    [1, 1, 1],\n  ],\n\n  // Waves\n  \"wave-lr\": [\n    [1, 1, 0],\n    [1, 1, 0],\n    [1, 1, 0],\n  ],\n  \"wave-rl\": [\n    [0, 1, 1],\n    [0, 1, 1],\n    [0, 1, 1],\n  ],\n  \"wave-tb\": [\n    [1, 1, 1],\n    [1, 1, 1],\n    [0, 0, 0],\n  ],\n  \"x-shape\": [\n    [1, 0, 1],\n    [0, 1, 0],\n    [1, 0, 1],\n  ],\n};\n\n// Color presets\nconst COLORS: Record<string, string> = {\n  amber: \"#fbbf24\",\n  blue: \"#38bdf8\",\n  green: \"#4ade80\",\n  red: \"#f87171\",\n  white: \"#f5f5f4\",\n};\n\n// Size presets in pixels\nconst SIZES: Record<string, number> = {\n  lg: 48,\n  md: 32,\n  sm: 24,\n  xl: 64,\n};\n\n// Speed presets in milliseconds\nconst SPEEDS: Record<string, number> = {\n  fast: 400,\n  normal: 800,\n  slow: 1500,\n};\n\n// Helper to resolve pattern to GridMatrix\nconst resolvePattern = (\n  pattern: PresetPattern | GridMatrix | undefined\n): GridMatrix => {\n  if (!pattern) {\n    return PATTERNS[\"plus-hollow\"];\n  }\n  if (typeof pattern === \"string\") {\n    return PATTERNS[pattern] ?? PATTERNS[\"plus-hollow\"];\n  }\n  return pattern;\n};\n\n// Helper to resolve color\nconst resolveColor = (color: string | undefined): string => {\n  if (!color) {\n    return COLORS.white;\n  }\n  return COLORS[color] ?? color;\n};\n\n// Helper to resolve size\nconst resolveSize = (\n  size: \"sm\" | \"md\" | \"lg\" | \"xl\" | number | undefined\n): number => {\n  if (size === undefined) {\n    return SIZES.md;\n  }\n  if (typeof size === \"number\") {\n    return size;\n  }\n  return SIZES[size] ?? SIZES.md;\n};\n\n// Cell component for individual grid cells\nconst GridCell = ({\n  active,\n  color,\n  cellSize,\n  animationDelay,\n  mode,\n  cycleDuration,\n  shouldReduceMotion,\n  rounded,\n}: {\n  active: boolean;\n  color: string;\n  cellSize: number;\n  animationDelay: number;\n  mode: \"pulse\" | \"sequence\" | \"stagger\";\n  cycleDuration: number;\n  shouldReduceMotion: boolean | null;\n  rounded?: boolean;\n}) => {\n  const glowSize = cellSize * 0.8;\n  const borderRadius = rounded ? \"50%\" : undefined;\n\n  if (!active) {\n    return (\n      <div\n        style={{\n          height: cellSize,\n          width: cellSize,\n        }}\n      />\n    );\n  }\n\n  // For reduced motion, show static cells\n  if (shouldReduceMotion) {\n    return (\n      <div\n        style={{\n          backgroundColor: color,\n          borderRadius,\n          boxShadow: `\n            0 0 ${glowSize * 0.3}px ${color},\n            0 0 ${glowSize * 0.6}px ${color}40,\n            0 0 ${glowSize * 1.2}px ${color}20\n          `,\n          height: cellSize,\n          width: cellSize,\n        }}\n      />\n    );\n  }\n\n  const pulseAnimation = {\n    opacity: [0.4, 1, 0.4],\n    scale: [0.95, 1, 0.95],\n  };\n\n  const staggerAnimation = {\n    opacity: [0, 1, 1, 0],\n    scale: [0.8, 1, 1, 0.8],\n  };\n\n  return (\n    <motion.div\n      animate={mode === \"stagger\" ? staggerAnimation : pulseAnimation}\n      style={{\n        backgroundColor: color,\n        borderRadius,\n        boxShadow: `\n          0 0 ${glowSize * 0.3}px ${color},\n          0 0 ${glowSize * 0.6}px ${color}40,\n          0 0 ${glowSize * 1.2}px ${color}20\n        `,\n        height: cellSize,\n        width: cellSize,\n      }}\n      transition={{\n        delay: mode === \"stagger\" ? animationDelay : 0,\n        duration: cycleDuration / 1000,\n        ease: [0.645, 0.045, 0.355, 1],\n        repeat: Number.POSITIVE_INFINITY,\n        times: mode === \"stagger\" ? [0, 0.2, 0.8, 1] : undefined,\n      }}\n    />\n  );\n};\n\nconst GridLoader = ({\n  pattern,\n  mode = \"pulse\",\n  sequence,\n  speed = \"normal\",\n  color,\n  size,\n  blur,\n  gap: gapProp,\n  rounded,\n  static: isStatic,\n  className,\n}: GridLoaderProps) => {\n  const shouldReduceMotion = useReducedMotion();\n  const [sequenceIndex, setSequenceIndex] = useState(0);\n\n  const sizeInPx = resolveSize(size);\n  const gapSize = gapProp ?? 0;\n  const cellSize = (sizeInPx - gapSize * 2) / 3;\n  const resolvedColor = resolveColor(color);\n  const cycleDuration = SPEEDS[speed] ?? SPEEDS.normal;\n  const disableAnimation = isStatic || shouldReduceMotion;\n\n  // Handle sequence mode\n  useEffect(() => {\n    if (mode !== \"sequence\" || !sequence || sequence.length === 0) {\n      return;\n    }\n    if (disableAnimation) {\n      return;\n    }\n\n    const interval = setInterval(() => {\n      setSequenceIndex((prev) => (prev + 1) % sequence.length);\n    }, cycleDuration);\n\n    return () => clearInterval(interval);\n  }, [mode, sequence, cycleDuration, disableAnimation]);\n\n  // Determine current grid to display\n  const currentGrid = useMemo(() => {\n    if (mode === \"sequence\" && sequence && sequence.length > 0) {\n      return resolvePattern(sequence[sequenceIndex]);\n    }\n    return resolvePattern(pattern);\n  }, [mode, sequence, sequenceIndex, pattern]);\n\n  // Flatten grid for rendering\n  const cells = currentGrid.flat();\n\n  // Calculate stagger delays based on active cells\n  const staggerDelays = useMemo(() => {\n    if (mode !== \"stagger\") {\n      return cells.map(() => 0);\n    }\n\n    const activeCells = cells.reduce<number[]>((acc, cell, idx) => {\n      if (cell === 1) {\n        acc.push(idx);\n      }\n      return acc;\n    }, []);\n\n    const delayPerCell = cycleDuration / 1000 / (activeCells.length + 2);\n\n    return cells.map((cell, idx) => {\n      if (cell === 0) {\n        return 0;\n      }\n      const activeIndex = activeCells.indexOf(idx);\n      return activeIndex * delayPerCell;\n    });\n  }, [cells, mode, cycleDuration]);\n\n  // Generate stable keys for grid positions (0-8 in a 3x3 grid)\n  const cellKeys = [\"tl\", \"tm\", \"tr\", \"ml\", \"mm\", \"mr\", \"bl\", \"bm\", \"br\"];\n\n  return (\n    <output\n      aria-label=\"Loading\"\n      className={cn(\"grid grid-cols-3\", className)}\n      style={{\n        filter: blur ? `blur(${blur}px)` : undefined,\n        gap: gapSize,\n        height: sizeInPx,\n        width: sizeInPx,\n      }}\n    >\n      {cells.map((active, idx) => (\n        <GridCell\n          active={active === 1}\n          animationDelay={staggerDelays[idx]}\n          cellSize={cellSize}\n          color={resolvedColor}\n          cycleDuration={cycleDuration}\n          key={cellKeys[idx]}\n          mode={mode}\n          rounded={rounded}\n          shouldReduceMotion={disableAnimation}\n        />\n      ))}\n    </output>\n  );\n};\n\nexport default GridLoader;\n\n// Export patterns for advanced usage\nexport { COLORS, PATTERNS, SIZES };\n","path":"index.tsx","target":"components/smoothui/grid-loader/index.tsx","type":"registry:ui"}],"name":"grid-loader","registryDependencies":[],"title":"Grid Loader","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion","lucide-react","react-use-measure"],"description":"A ImageMetadataPreview component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { ChevronUp, CircleX, Share } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useEffect, useState } from \"react\";\nimport useMeasure from \"react-use-measure\";\n\nexport interface ImageMetadata {\n  by: string;\n  created: string;\n  source: string;\n  updated: string;\n}\n\nexport interface ImageMetadataPreviewProps {\n  alt?: string;\n  description?: string;\n  filename?: string;\n  imageSrc: string;\n  metadata: ImageMetadata;\n  onShare?: () => void;\n}\n\nexport default function ImageMetadataPreview({\n  imageSrc,\n  alt = \"Image preview\",\n  filename = \"screenshot.png\",\n  description = \"No description\",\n  metadata,\n  onShare,\n}: ImageMetadataPreviewProps) {\n  const [openInfo, setopenInfo] = useState(false);\n  const [isHoverDevice, setIsHoverDevice] = useState(false);\n  const [elementRef, bounds] = useMeasure();\n  const shouldReduceMotion = useReducedMotion();\n\n  useEffect(() => {\n    const mediaQuery = window.matchMedia(\"(hover: hover) and (pointer: fine)\");\n    setIsHoverDevice(mediaQuery.matches);\n\n    const handleChange = (e: MediaQueryListEvent) => {\n      setIsHoverDevice(e.matches);\n    };\n\n    mediaQuery.addEventListener(\"change\", handleChange);\n    return () => mediaQuery.removeEventListener(\"change\", handleChange);\n  }, []);\n\n  const handleClickOpen = () => {\n    setopenInfo((b) => !b);\n  };\n\n  const handleClickClose = () => {\n    setopenInfo((b) => !b);\n  };\n\n  return (\n    <div className=\"absolute bottom-10 flex flex-col items-center justify-center gap-4\">\n      <motion.div\n        animate={shouldReduceMotion ? {} : { y: -bounds.height }}\n        className=\"pointer-events-none overflow-hidden rounded-xl\"\n        transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.25 }}\n      >\n        <img\n          alt={alt}\n          draggable={false}\n          height={437}\n          src={imageSrc}\n          width={300}\n        />\n      </motion.div>\n\n      <div className=\"relative flex w-full flex-col items-center gap-4\">\n        <div className=\"relative flex w-full flex-row items-center justify-center gap-4\">\n          <button\n            aria-label=\"Share\"\n            className={`min-h-[44px] min-w-[44px] rounded-full border bg-background p-3 transition focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ${\n              isHoverDevice ? \"hover:bg-muted\" : \"\"\n            }`}\n            disabled={!onShare}\n            onClick={onShare}\n            type=\"button\"\n          >\n            <Share aria-hidden=\"true\" size={16} />\n          </button>\n          <button\n            aria-label=\"Connect\"\n            className=\"min-h-[44px] cursor-not-allowed rounded-full border bg-background px-4 py-3 text-sm transition focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50\"\n            disabled\n            type=\"button\"\n          >\n            Connect\n          </button>\n          <AnimatePresence>\n            {openInfo ? null : (\n              <motion.button\n                animate={\n                  shouldReduceMotion\n                    ? { opacity: 1 }\n                    : { filter: \"blur(0px)\", opacity: 1 }\n                }\n                aria-label=\"Open Metadata Preview\"\n                className={`min-h-[44px] min-w-[44px] border bg-background p-3 shadow-xs transition focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ${\n                  isHoverDevice ? \"hover:bg-muted\" : \"\"\n                }`}\n                initial={\n                  shouldReduceMotion\n                    ? { opacity: 1 }\n                    : { filter: \"blur(4px)\", opacity: 0 }\n                }\n                onClick={handleClickOpen}\n                style={{ borderRadius: 100 }}\n                transition={\n                  shouldReduceMotion ? { duration: 0 } : { duration: 0.2 }\n                }\n              >\n                <ChevronUp aria-hidden=\"true\" size={16} />\n              </motion.button>\n            )}\n          </AnimatePresence>\n        </div>\n        <AnimatePresence>\n          {openInfo ? (\n            <motion.div\n              animate={\n                shouldReduceMotion\n                  ? { opacity: 1 }\n                  : { filter: \"blur(0px)\", opacity: 1 }\n              }\n              className=\"absolute bottom-0 w-full cursor-pointer gap-4 border bg-background p-5 shadow-xs\"\n              initial={\n                shouldReduceMotion\n                  ? { opacity: 1 }\n                  : { filter: \"blur(4px)\", opacity: 0 }\n              }\n              onClick={handleClickClose}\n              style={{ borderRadius: 20 }}\n              transition={\n                shouldReduceMotion\n                  ? { duration: 0 }\n                  : { bounce: 0, duration: 0.25, type: \"spring\" as const }\n              }\n            >\n              <div className=\"flex flex-col items-start\" ref={elementRef}>\n                <div className=\"flex w-full flex-row items-start justify-between gap-4\">\n                  <div>\n                    <p className=\"text-foreground\">{filename}</p>\n                    <p className=\"text-primary-foreground\">{description}</p>\n                  </div>\n\n                  <button\n                    aria-label=\"Close metadata preview\"\n                    className={`flex min-h-[44px] min-w-[44px] items-center justify-center rounded p-2 transition-colors focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ${\n                      isHoverDevice ? \"hover:bg-muted\" : \"\"\n                    }`}\n                    onClick={(e) => {\n                      e.stopPropagation();\n                      handleClickClose();\n                    }}\n                    type=\"button\"\n                  >\n                    <CircleX aria-hidden=\"true\" size={16} />\n                  </button>\n                </div>\n                <table className=\"flex w-full flex-col items-center gap-4 text-foreground\">\n                  <tbody className=\"w-full\">\n                    <tr className=\"flex w-full flex-row items-center gap-4\">\n                      <td className=\"w-1/2\">Created</td>\n                      <td className=\"w-1/2 text-primary-foreground\">\n                        {metadata.created}\n                      </td>\n                    </tr>\n                    <tr className=\"flex w-full flex-row items-center gap-4\">\n                      <td className=\"w-1/2\">Updated</td>\n                      <td className=\"w-1/2 text-primary-foreground\">\n                        {metadata.updated}\n                      </td>\n                    </tr>\n                    <tr className=\"flex w-full flex-row items-center gap-4\">\n                      <td className=\"w-1/2\">By</td>\n                      <td className=\"w-1/2\">{metadata.by}</td>\n                    </tr>\n                    <tr className=\"flex w-full flex-row items-center gap-4\">\n                      <td className=\"w-1/2\">Source</td>\n                      <td className=\"w-1/2 truncate\">{metadata.source}</td>\n                    </tr>\n                  </tbody>\n                </table>\n              </div>\n            </motion.div>\n          ) : null}\n        </AnimatePresence>\n      </div>\n    </div>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/image-metadata-preview/index.tsx","type":"registry:ui"}],"name":"image-metadata-preview","registryDependencies":[],"title":"Image Metadata Preview","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion","react-use-measure"],"description":"A InfiniteSlider component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport {\n  animate,\n  motion,\n  useMotionValue,\n  useReducedMotion,\n} from \"motion/react\";\nimport { useEffect, useState } from \"react\";\nimport useMeasure from \"react-use-measure\";\n\nexport interface InfiniteSliderProps {\n  children: React.ReactNode;\n  className?: string;\n  direction?: \"horizontal\" | \"vertical\";\n  gap?: number;\n  reverse?: boolean;\n  speed?: number;\n  speedOnHover?: number;\n}\n\nexport default function InfiniteSlider({\n  children,\n  gap = 16,\n  speed = 100,\n  speedOnHover,\n  direction = \"horizontal\",\n  reverse = false,\n  className,\n}: InfiniteSliderProps) {\n  const [currentSpeed, setCurrentSpeed] = useState(speed);\n  const [ref, { width, height }] = useMeasure();\n  const translation = useMotionValue(0);\n  const [isTransitioning, setIsTransitioning] = useState(false);\n  const [_key, setKey] = useState(0);\n  const shouldReduceMotion = useReducedMotion();\n\n  useEffect(() => {\n    if (shouldReduceMotion) {\n      translation.set(0);\n      return;\n    }\n\n    let controls:\n      | {\n          stop: () => void;\n        }\n      | undefined;\n    const size = direction === \"horizontal\" ? width : height;\n    const contentSize = size + gap;\n    const from = reverse ? -contentSize / 2 : 0;\n    const to = reverse ? 0 : -contentSize / 2;\n\n    const distanceToTravel = Math.abs(to - from);\n    const duration = distanceToTravel / currentSpeed;\n\n    if (isTransitioning) {\n      const remainingDistance = Math.abs(translation.get() - to);\n      const transitionDuration = remainingDistance / currentSpeed;\n\n      controls = animate(translation, [translation.get(), to], {\n        duration: transitionDuration,\n        ease: \"linear\",\n        onComplete: () => {\n          setIsTransitioning(false);\n          setKey((prevKey) => prevKey + 1);\n        },\n      });\n    } else {\n      controls = animate(translation, [from, to], {\n        duration,\n        ease: \"linear\",\n        onRepeat: () => {\n          translation.set(from);\n        },\n        repeat: Number.POSITIVE_INFINITY,\n        repeatDelay: 0,\n        repeatType: \"loop\",\n      });\n    }\n\n    return controls?.stop;\n  }, [\n    translation,\n    currentSpeed,\n    width,\n    height,\n    gap,\n    isTransitioning,\n    direction,\n    reverse,\n    shouldReduceMotion,\n  ]);\n\n  const hoverProps = speedOnHover\n    ? {\n        onHoverEnd: () => {\n          setIsTransitioning(true);\n          setCurrentSpeed(speed);\n        },\n        onHoverStart: () => {\n          setIsTransitioning(true);\n          setCurrentSpeed(speedOnHover);\n        },\n      }\n    : {};\n\n  return (\n    <div className={cn(\"overflow-hidden\", className)}>\n      <motion.div\n        className=\"flex w-max\"\n        ref={ref}\n        style={{\n          ...(direction === \"horizontal\"\n            ? { x: translation }\n            : { y: translation }),\n          flexDirection: direction === \"horizontal\" ? \"row\" : \"column\",\n          gap: `${gap}px`,\n        }}\n        {...hoverProps}\n      >\n        {children}\n        {children}\n      </motion.div>\n    </div>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/infinite-slider/index.tsx","type":"registry:ui"}],"name":"infinite-slider","registryDependencies":[],"title":"Infinite Slider","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion","lucide-react"],"description":"A InteractiveImageSelector component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { Share2, Trash2 } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useCallback, useState } from \"react\";\n\nconst RESET_DELAY = 200;\nconst RESET_SCALE_START = 1;\nconst RESET_SCALE_PEAK = 1.1;\nconst RESET_SCALE_END = 1;\nconst RESET_ROTATE_START = 0;\nconst RESET_ROTATE_POSITIVE = 5;\nconst RESET_ROTATE_NEGATIVE = -5;\nconst SELECT_SCALE_START = 1;\nconst SELECT_SCALE_PEAK = 1.1;\nconst SELECT_SCALE_END = 1;\nconst SELECT_ROTATE_START = 0;\nconst SELECT_ROTATE_NEGATIVE = -5;\nconst SELECT_ROTATE_POSITIVE = 5;\nconst CONTAINER_SCALE_START = 1;\nconst CONTAINER_SCALE_MIN = 0.95;\nconst CONTAINER_SCALE_END = 1;\nconst ITEM_SCALE_START = 1;\nconst ITEM_SCALE_MIN = 0.9;\nconst ITEM_SCALE_END = 1;\nconst ITEM_ROTATE_START = 0;\nconst ITEM_ROTATE_POSITIVE = 2;\nconst ITEM_ROTATE_NEGATIVE = -2;\nconst RESET_ANIMATION_DURATION = 0.3;\n\nexport interface ImageData {\n  id: number;\n  src: string;\n}\n\nexport interface InteractiveImageSelectorProps {\n  className?: string;\n  images: ImageData[];\n  onChange?: (selected: number[]) => void;\n  onDelete?: (deleted: number[]) => void;\n  onShare?: (selected: number[]) => void;\n  selectable?: boolean;\n  selectedImages?: number[];\n}\n\nexport default function InteractiveImageSelector({\n  images,\n  selectedImages: controlledSelected,\n  onChange,\n  onDelete,\n  onShare,\n  className = \"\",\n  selectable = false,\n}: InteractiveImageSelectorProps) {\n  const [originalImages] = useState<ImageData[]>(images);\n  const [internalImages, setInternalImages] = useState<ImageData[]>(images);\n  const [internalSelected, setInternalSelected] = useState<number[]>([]);\n  const [isSelecting, setIsSelecting] = useState(selectable);\n  const [isResetting, setIsResetting] = useState(false);\n  const shouldReduceMotion = useReducedMotion();\n\n  const selected = controlledSelected ?? internalSelected;\n\n  const handleImageClick = useCallback(\n    (id: number) => {\n      if (!isSelecting) {\n        return;\n      }\n      const newSelected = selected.includes(id)\n        ? selected.filter((imgId) => imgId !== id)\n        : [...selected, id];\n      if (onChange) {\n        onChange(newSelected);\n      } else {\n        setInternalSelected(newSelected);\n      }\n    },\n    [isSelecting, selected, onChange]\n  );\n\n  const handleDelete = useCallback(() => {\n    const newImages = internalImages.filter(\n      (img) => !selected.includes(img.id)\n    );\n    if (onDelete) {\n      onDelete(selected);\n    }\n    setInternalImages(newImages);\n    if (onChange) {\n      onChange([]);\n    } else {\n      setInternalSelected([]);\n    }\n  }, [selected, internalImages, onDelete, onChange]);\n\n  const handleReset = useCallback(() => {\n    setIsResetting(true);\n\n    // Add a small delay to show the reset animation\n    setTimeout(() => {\n      setInternalImages(originalImages);\n      if (onChange) {\n        onChange([]);\n      } else {\n        setInternalSelected([]);\n      }\n      setIsSelecting(false);\n      setIsResetting(false);\n    }, RESET_DELAY);\n  }, [originalImages, onChange]);\n\n  const toggleSelecting = useCallback(() => {\n    setIsSelecting((prev) => !prev);\n    if (isSelecting) {\n      if (onChange) {\n        onChange([]);\n      } else {\n        setInternalSelected([]);\n      }\n    }\n  }, [isSelecting, onChange]);\n\n  const handleShare = useCallback(() => {\n    if (onShare) {\n      onShare(selected);\n    }\n  }, [onShare, selected]);\n\n  return (\n    <div\n      className={`relative flex h-full w-full max-w-[500px] flex-col justify-between p-4 ${className}`}\n    >\n      <div className=\"pointer-events-none absolute inset-x-0 top-0 z-10 h-28 bg-linear-to-b from-primary/80 to-transparent dark:from-background/50\" />\n      <div className=\"absolute top-5 right-5 left-5 z-20 flex justify-between p-4\">\n        <motion.button\n          animate={\n            shouldReduceMotion || !isResetting\n              ? {}\n              : {\n                  rotate: [\n                    RESET_ROTATE_START,\n                    RESET_ROTATE_POSITIVE,\n                    RESET_ROTATE_NEGATIVE,\n                    RESET_ROTATE_START,\n                  ],\n                  scale: [RESET_SCALE_START, RESET_SCALE_PEAK, RESET_SCALE_END],\n                }\n          }\n          aria-label=\"Reset selection\"\n          className={`cursor-pointer rounded-full px-3 py-1 font-semibold text-sm bg-blend-luminosity backdrop-blur-xl transition-colors ${\n            isResetting\n              ? \"bg-brand/30 text-white\"\n              : \"bg-background/20 text-foreground\"\n          }`}\n          disabled={isResetting}\n          exit={shouldReduceMotion ? {} : { rotate: 0 }}\n          initial={shouldReduceMotion ? {} : { rotate: 0 }}\n          onClick={handleReset}\n          transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.25 }}\n          whileHover={shouldReduceMotion ? {} : { scale: 1.05 }}\n          whileTap={shouldReduceMotion ? {} : { scale: 0.95 }}\n        >\n          {isResetting ? \"Resetting...\" : \"Reset\"}\n        </motion.button>\n        <motion.button\n          animate={\n            isSelecting\n              ? {\n                  rotate: [\n                    SELECT_ROTATE_START,\n                    SELECT_ROTATE_NEGATIVE,\n                    SELECT_ROTATE_POSITIVE,\n                    SELECT_ROTATE_START,\n                  ],\n                  scale: [\n                    SELECT_SCALE_START,\n                    SELECT_SCALE_PEAK,\n                    SELECT_SCALE_END,\n                  ],\n                }\n              : {}\n          }\n          aria-label={isSelecting ? \"Cancel selection\" : \"Select images\"}\n          className={`cursor-pointer rounded-full px-3 py-1 font-semibold text-sm bg-blend-luminosity backdrop-blur-xl ${\n            isSelecting\n              ? \"bg-brand/30 text-white\"\n              : \"bg-background/20 text-foreground\"\n          }`}\n          exit={{ rotate: 0 }}\n          initial={{ rotate: 0 }}\n          onClick={toggleSelecting}\n          transition={{ duration: 0.3 }}\n          type=\"button\"\n          whileHover={{ scale: 1.05 }}\n          whileTap={{ scale: 0.95 }}\n        >\n          {isSelecting ? \"Cancel\" : \"Select\"}\n        </motion.button>\n      </div>\n\n      <motion.div\n        animate={\n          shouldReduceMotion || !isResetting\n            ? {}\n            : {\n                scale: [\n                  CONTAINER_SCALE_START,\n                  CONTAINER_SCALE_MIN,\n                  CONTAINER_SCALE_END,\n                ],\n              }\n        }\n        className=\"grid grid-cols-3 gap-1 overflow-scroll\"\n        layout={!shouldReduceMotion}\n        transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.2 }}\n      >\n        <AnimatePresence>\n          {internalImages.map((img) => (\n            <motion.div\n              animate={{\n                opacity: 1,\n                rotate: isResetting\n                  ? [\n                      ITEM_ROTATE_START,\n                      ITEM_ROTATE_POSITIVE,\n                      ITEM_ROTATE_NEGATIVE,\n                      ITEM_ROTATE_START,\n                    ]\n                  : 0,\n                scale: isResetting\n                  ? [ITEM_SCALE_START, ITEM_SCALE_MIN, ITEM_SCALE_END]\n                  : 1,\n              }}\n              className=\"relative aspect-square cursor-pointer\"\n              exit={{ opacity: 0, scale: 0.8 }}\n              initial={{ opacity: 0, scale: 0.8 }}\n              key={img.id}\n              layout\n              onClick={() => handleImageClick(img.id)}\n              transition={{\n                damping: 25,\n                duration: isResetting ? RESET_ANIMATION_DURATION : undefined,\n                stiffness: 300,\n                type: \"spring\" as const,\n              }}\n            >\n              <img\n                alt={`Gallery item ${img.id}`}\n                className={`h-full w-full rounded-lg object-cover ${\n                  selected.includes(img.id) && isSelecting ? \"opacity-75\" : \"\"\n                }`}\n                draggable={false}\n                height={200}\n                loading=\"lazy\"\n                src={img.src}\n                width={200}\n              />\n              {isSelecting && selected.includes(img.id) && (\n                <div className=\"absolute right-2 bottom-2 flex h-6 w-6 items-center justify-center rounded-full border border-white bg-brand text-white\">\n                  ✓\n                </div>\n              )}\n            </motion.div>\n          ))}\n        </AnimatePresence>\n      </motion.div>\n      <AnimatePresence>\n        {isSelecting ? (\n          <motion.div\n            animate={shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }}\n            className=\"absolute right-2 bottom-0 left-1/2 z-10 flex w-2/3 -translate-x-1/2 items-center justify-between rounded-full bg-background/20 p-4 bg-blend-luminosity backdrop-blur-md\"\n            exit={\n              shouldReduceMotion\n                ? { opacity: 0, transition: { duration: 0 } }\n                : { opacity: 0, y: 20 }\n            }\n            initial={\n              shouldReduceMotion ? { opacity: 1 } : { opacity: 0, y: 20 }\n            }\n            transition={\n              shouldReduceMotion ? { duration: 0 } : { duration: 0.25 }\n            }\n          >\n            <button\n              className=\"cursor-pointer text-brand\"\n              onClick={handleShare}\n              type=\"button\"\n            >\n              <Share2 size={24} />\n            </button>\n            <span className=\"text-foreground\">{selected.length} selected</span>\n            <button\n              className=\"cursor-pointer text-brand\"\n              disabled={selected.length === 0}\n              onClick={handleDelete}\n              type=\"button\"\n            >\n              <Trash2 size={24} />\n            </button>\n          </motion.div>\n        ) : null}\n      </AnimatePresence>\n    </div>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/interactive-image-selector/index.tsx","type":"registry:ui"}],"name":"interactive-image-selector","registryDependencies":["https://smoothui.dev/r/tokens.json"],"title":"Interactive Image Selector","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion","usehooks-ts"],"description":"A JobListingComponent component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport type { SVGProps } from \"react\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { useOnClickOutside } from \"usehooks-ts\";\n\nexport interface Job {\n  company: string;\n  job_description: string;\n  job_time: string;\n  location: string;\n  logo: React.ReactNode;\n  remote: string;\n  salary: string;\n  title: string;\n}\n\nexport interface JobListingComponentProps {\n  className?: string;\n  jobs: Job[];\n  onJobClick?: (job: Job) => void;\n}\n\nexport const Resend = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    fill=\"none\"\n    height=\"1em\"\n    viewBox=\"0 0 600 600\"\n    width=\"1em\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    {...props}\n  >\n    <title>Resend logo</title>\n    <path\n      d=\"M186 447.471V154H318.062C336.788 154 353.697 158.053 368.79 166.158C384.163 174.263 396.181 185.443 404.845 199.698C413.51 213.672 417.842 229.604 417.842 247.491C417.842 265.938 413.51 282.568 404.845 297.381C396.181 311.915 384.302 323.375 369.209 331.759C354.117 340.144 337.067 344.337 318.062 344.337H253.917V447.471H186ZM348.667 447.471L274.041 314.99L346.99 304.509L430 447.471H348.667ZM253.917 289.835H311.773C319.04 289.835 325.329 288.298 330.639 285.223C336.229 281.869 340.421 277.258 343.216 271.388C346.291 265.519 347.828 258.811 347.828 251.265C347.828 243.718 346.151 237.15 342.797 231.56C339.443 225.691 334.552 221.219 328.124 218.144C321.975 215.07 314.428 213.533 305.484 213.533H253.917V289.835Z\"\n      fill=\"currentColor\"\n    />\n  </svg>\n);\n\nexport const Turso = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    fill=\"none\"\n    height=\"1em\"\n    viewBox=\"0 0 201 170\"\n    width=\"1em\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    {...props}\n  >\n    <title>Turso logo</title>\n    <path\n      d=\"m100.055 170c-2.1901 0-18.2001-12.8-21.3001-16.45-2.44 3.73-6.44 7.96-6.44 7.96-11.05-5.57-25.17-20.06-27.83-25.13-2.62-5-12.13-62.58-12.39-79.3-.34-9.41 5.85-28.49 67.9601-28.49 62.11 0 68.29 19.08 67.96 28.49-.25 16.72-9.76 74.3-12.39 79.3-2.66 5.07-16.78 19.56-27.83 25.13 0 0-4-4.23-6.44-7.96-3.1 3.65-19.11 16.45-21.3 16.45z\"\n      fill=\"#1ebca1\"\n    />\n    <path\n      d=\"m100.055 132.92c-20.7301 0-33.9601-10.95-33.9601-10.95l1.91-26.67-21.75-1.94-3.91-31.55h115.4301l-3.91 31.55-21.75 1.94 1.91 26.67s-13.23 10.95-33.96 10.95z\"\n      fill=\"#183134\"\n    />\n    <path\n      d=\"m121.535 75.79 78.52-27.18c-4.67-27.94-29.16-48.61-29.16-48.61v30.78l-14.54 3.75-9.11-10.97-7.8 15.34-39.38 10.16-39.3801-10.16-7.8-15.34-9.11 10.97-14.54-3.75v-30.78s-24.50997 20.67-29.1799684 48.61l78.5199684 27.18-2.8 37.39c6.7 1.7 13.75 3.39 24.2801 3.39 10.53 0 17.57-1.69 24.27-3.39l-2.8-37.39z\"\n      fill=\"#4ff8d2\"\n    />\n  </svg>\n);\n\nexport const Supabase = (props: SVGProps<SVGSVGElement>) => (\n  <svg\n    fill=\"none\"\n    height=\"1em\"\n    viewBox=\"0 0 109 113\"\n    width=\"1em\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    {...props}\n  >\n    <title>Supabase logo</title>\n    <path\n      d=\"M63.7076 110.284C60.8481 113.885 55.0502 111.912 54.9813 107.314L53.9738 40.0627L99.1935 40.0627C107.384 40.0627 111.952 49.5228 106.859 55.9374L63.7076 110.284Z\"\n      fill=\"url(#paint0_linear)\"\n    />\n    <path\n      d=\"M63.7076 110.284C60.8481 113.885 55.0502 111.912 54.9813 107.314L53.9738 40.0627L99.1935 40.0627C107.384 40.0627 111.952 49.5228 106.859 55.9374L63.7076 110.284Z\"\n      fill=\"url(#paint1_linear)\"\n      fillOpacity={0.2}\n    />\n    <path\n      d=\"M45.317 2.07103C48.1765 -1.53037 53.9745 0.442937 54.0434 5.041L54.4849 72.2922H9.83113C1.64038 72.2922 -2.92775 62.8321 2.1655 56.4175L45.317 2.07103Z\"\n      fill=\"#3ECF8E\"\n    />\n    <defs>\n      <linearGradient\n        gradientUnits=\"userSpaceOnUse\"\n        id=\"paint0_linear\"\n        x1={53.9738}\n        x2={94.1635}\n        y1={54.974}\n        y2={71.8295}\n      >\n        <stop stopColor=\"#249361\" />\n        <stop offset={1} stopColor=\"#3ECF8E\" />\n      </linearGradient>\n      <linearGradient\n        gradientUnits=\"userSpaceOnUse\"\n        id=\"paint1_linear\"\n        x1={36.1558}\n        x2={54.4844}\n        y1={30.578}\n        y2={65.0806}\n      >\n        <stop />\n        <stop offset={1} stopOpacity={0} />\n      </linearGradient>\n    </defs>\n  </svg>\n);\n\nexport default function JobListingComponent({\n  jobs,\n  className,\n  onJobClick,\n}: JobListingComponentProps) {\n  const [activeItem, setActiveItem] = useState<Job | null>(null);\n  const ref = useRef<HTMLDivElement>(null) as React.RefObject<HTMLDivElement>;\n  const shouldReduceMotion = useReducedMotion();\n  useOnClickOutside(ref, () => setActiveItem(null));\n\n  useEffect(() => {\n    function onKeyDown(event: { key: string }) {\n      if (event.key === \"Escape\") {\n        setActiveItem(null);\n      }\n    }\n\n    window.addEventListener(\"keydown\", onKeyDown);\n    return () => window.removeEventListener(\"keydown\", onKeyDown);\n  }, []);\n\n  return (\n    <>\n      <AnimatePresence>\n        {activeItem ? (\n          <motion.div\n            animate={shouldReduceMotion ? { opacity: 1 } : { opacity: 1 }}\n            className=\"pointer-events-none absolute inset-0 z-10 bg-smooth-1000/10 bg-blend-luminosity backdrop-blur-xl\"\n            exit={\n              shouldReduceMotion\n                ? { opacity: 0, transition: { duration: 0 } }\n                : { opacity: 0 }\n            }\n            initial={shouldReduceMotion ? { opacity: 1 } : { opacity: 0 }}\n            transition={\n              shouldReduceMotion\n                ? { duration: 0 }\n                : { duration: 0.2, ease: [0.215, 0.61, 0.355, 1] }\n            }\n          />\n        ) : null}\n      </AnimatePresence>\n      <AnimatePresence>\n        {activeItem ? (\n          <div className=\"group absolute inset-0 z-10 grid place-items-center\">\n            <motion.div\n              className=\"flex h-fit w-[90%] max-w-2xl cursor-pointer select-none flex-col items-start gap-4 overflow-hidden border bg-background p-4 shadow-xs\"\n              layoutId={\n                shouldReduceMotion\n                  ? undefined\n                  : `workItem-${activeItem.company}`\n              }\n              ref={ref}\n              style={{\n                borderRadius: 12,\n                willChange: shouldReduceMotion ? \"auto\" : \"transform\",\n              }}\n              transition={\n                shouldReduceMotion\n                  ? { duration: 0 }\n                  : {\n                      bounce: 0.1,\n                      duration: 0.25,\n                      layout: {\n                        duration: 0.25,\n                        ease: [0.645, 0.045, 0.355, 1],\n                      },\n                      type: \"spring\" as const,\n                    }\n              }\n            >\n              <div className=\"relative flex w-full items-center gap-4\">\n                <motion.div\n                  layoutId={\n                    shouldReduceMotion\n                      ? undefined\n                      : `workItemLogo-${activeItem.company}`\n                  }\n                  style={{\n                    flexShrink: 0,\n                    willChange: shouldReduceMotion ? \"auto\" : \"transform\",\n                  }}\n                >\n                  {activeItem.logo}\n                </motion.div>\n                <div className=\"flex min-w-0 grow items-center justify-between\">\n                  <div className=\"flex min-w-0 flex-col gap-0.5\">\n                    <div className=\"flex w-full flex-row justify-between gap-0.5\">\n                      <div className=\"font-medium text-foreground text-sm\">\n                        {activeItem.company}\n                      </div>\n                    </div>\n                    <p className=\"text-primary-foreground text-sm\">\n                      {activeItem.title} / {activeItem.salary}\n                    </p>\n                    <div className=\"flex min-w-0 flex-row flex-wrap gap-2 text-primary-foreground text-xs\">\n                      {activeItem.remote === \"Yes\" &&\n                        ` ${activeItem.location} `}\n                      {activeItem.remote === \"No\" && ` ${activeItem.location} `}\n                      {activeItem.remote === \"Hybrid\" &&\n                        ` ${activeItem.remote} / ${activeItem.location} `}\n                      | {activeItem.job_time}\n                    </div>\n                  </div>\n                </div>\n              </div>\n              <motion.p\n                animate={shouldReduceMotion ? { opacity: 1 } : { opacity: 1 }}\n                className=\"text-primary-foreground text-sm\"\n                exit={\n                  shouldReduceMotion\n                    ? { opacity: 0, transition: { duration: 0 } }\n                    : { opacity: 0 }\n                }\n                initial={shouldReduceMotion ? { opacity: 1 } : { opacity: 0 }}\n                transition={\n                  shouldReduceMotion\n                    ? { duration: 0 }\n                    : {\n                        delay: 0.05,\n                        duration: 0.2,\n                        ease: [0.215, 0.61, 0.355, 1],\n                      }\n                }\n              >\n                {activeItem.job_description}\n              </motion.p>\n            </motion.div>\n          </div>\n        ) : null}\n      </AnimatePresence>\n      <div className={`relative flex items-start p-6 ${className || \"\"}`}>\n        <div className=\"relative flex w-full flex-col items-center gap-4 px-2\">\n          {jobs.map((role) => (\n            <motion.div\n              className=\"group relative flex w-full cursor-pointer select-none flex-row items-center gap-4 overflow-hidden border bg-background p-2 shadow-xs md:p-4\"\n              key={role.company}\n              layoutId={\n                shouldReduceMotion ? undefined : `workItem-${role.company}`\n              }\n              onClick={() => {\n                setActiveItem(role);\n                if (onJobClick) {\n                  onJobClick(role);\n                }\n              }}\n              style={{\n                borderRadius: 8,\n                willChange: shouldReduceMotion ? \"auto\" : \"transform\",\n              }}\n              transition={\n                shouldReduceMotion\n                  ? { duration: 0 }\n                  : {\n                      bounce: 0.1,\n                      duration: 0.25,\n                      layout: {\n                        duration: 0.25,\n                        ease: [0.645, 0.045, 0.355, 1],\n                      },\n                      type: \"spring\" as const,\n                    }\n              }\n              whileTap={shouldReduceMotion ? undefined : { scale: 0.97 }}\n            >\n              <motion.div\n                layoutId={\n                  shouldReduceMotion\n                    ? undefined\n                    : `workItemLogo-${role.company}`\n                }\n                style={{\n                  flexShrink: 0,\n                  willChange: shouldReduceMotion ? \"auto\" : \"transform\",\n                }}\n              >\n                {role.logo}\n              </motion.div>\n              <div className=\"flex w-full flex-col items-start justify-between gap-0.5\">\n                <div className=\"font-medium text-foreground\">\n                  {role.company}\n                </div>\n                <div className=\"text-primary-foreground text-xs\">\n                  {role.title} / {role.salary}\n                </div>\n\n                <div className=\"flex min-w-0 flex-row flex-wrap gap-2 text-primary-foreground text-xs\">\n                  {role.remote === \"Yes\" && ` ${role.location} `}\n                  {role.remote === \"No\" && ` ${role.location} `}\n                  {role.remote === \"Hybrid\" &&\n                    ` ${role.remote} / ${role.location} `}\n                  | {role.job_time}\n                </div>\n              </div>\n              <div\n                style={{\n                  opacity: 0,\n                  pointerEvents: \"none\",\n                  position: \"absolute\",\n                  top: \"100%\",\n                }}\n              >\n                {role.job_description}\n              </div>\n            </motion.div>\n          ))}\n        </div>\n      </div>\n    </>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/job-listing-component/index.tsx","type":"registry:ui"}],"name":"job-listing-component","registryDependencies":["https://smoothui.dev/r/tokens.json"],"title":"Job Listing Component","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"A KineticCenterBuild text animation component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useEffect, useState } from \"react\";\n\nexport interface KineticCenterBuildProps {\n  className?: string;\n  /** Interval between phrase completions in milliseconds. */\n  interval?: number;\n  phrases: string[];\n}\n\nconst BUILD_EASE = [0.2, 0.8, 0.2, 1] as const;\nconst EXIT_EASE = [0.4, 0, 0.2, 1] as const;\n\n/**\n * KineticCenterBuild — Apple keynote-style word-by-word build.\n * Each new word enters from the right and pushes the line until the full phrase locks centered.\n * From the animate-text catalog (`kinetic-center-build`).\n */\nexport default function KineticCenterBuild({\n  phrases,\n  className = \"\",\n  interval = 2500,\n}: KineticCenterBuildProps) {\n  const [phraseIndex, setPhraseIndex] = useState(0);\n  const [wordCount, setWordCount] = useState(1);\n  const [exiting, setExiting] = useState(false);\n  const shouldReduceMotion = useReducedMotion();\n\n  const currentPhrase = phrases[phraseIndex] ?? \"\";\n  const words = currentPhrase.split(\" \");\n\n  // biome-ignore lint/correctness/useExhaustiveDependencies: words.length drives the build schedule\n  useEffect(() => {\n    if (shouldReduceMotion) {\n      const holdId = setTimeout(() => {\n        setPhraseIndex((prev) => (prev + 1) % phrases.length);\n      }, interval);\n      return () => clearTimeout(holdId);\n    }\n\n    setWordCount(1);\n    setExiting(false);\n\n    const buildTimers: ReturnType<typeof setTimeout>[] = [];\n\n    for (let i = 1; i < words.length; i++) {\n      buildTimers.push(\n        setTimeout(() => {\n          setWordCount(i + 1);\n        }, i * 430)\n      );\n    }\n\n    const totalBuild = (words.length - 1) * 430 + 340;\n    const holdId = setTimeout(() => {\n      setExiting(true);\n      setTimeout(() => {\n        setPhraseIndex((prev) => (prev + 1) % phrases.length);\n        setWordCount(1);\n        setExiting(false);\n      }, 260 + 220);\n    }, totalBuild + interval);\n\n    buildTimers.push(holdId);\n    return () => {\n      for (const t of buildTimers) {\n        clearTimeout(t);\n      }\n    };\n  }, [phraseIndex, phrases.length, interval, shouldReduceMotion, words.length]);\n\n  const visibleWords = shouldReduceMotion ? words : words.slice(0, wordCount);\n  const restingAnimate = shouldReduceMotion\n    ? { opacity: 1 }\n    : { filter: \"blur(0px)\", opacity: 1, scale: 1, x: 0, y: 0 };\n  const exitAnimate = {\n    filter: \"blur(2.5px)\",\n    opacity: 0,\n    transition: { duration: 0.26, ease: EXIT_EASE },\n    y: -6,\n  };\n\n  return (\n    <span\n      aria-live=\"polite\"\n      className={`flex flex-wrap items-center justify-center ${className}`}\n      style={{ gap: 10 }}\n    >\n      <AnimatePresence mode=\"popLayout\">\n        {visibleWords.map((word, i) => (\n          <motion.span\n            animate={exiting ? exitAnimate : restingAnimate}\n            initial={\n              shouldReduceMotion\n                ? { opacity: 1 }\n                : {\n                    filter: \"blur(3.5px)\",\n                    opacity: 0,\n                    scale: 0.992,\n                    x: 88,\n                    y: 6,\n                  }\n            }\n            // biome-ignore lint/suspicious/noArrayIndexKey: word position is the stable identity within a phrase\n            key={`${phraseIndex}-${i}`}\n            layout\n            style={{ display: \"inline-block\" }}\n            transition={\n              shouldReduceMotion\n                ? { duration: 0 }\n                : {\n                    duration: i === 0 ? 0.34 : 0.43,\n                    ease: BUILD_EASE,\n                    layout: { duration: 0.43, ease: BUILD_EASE },\n                  }\n            }\n          >\n            {word}\n          </motion.span>\n        ))}\n      </AnimatePresence>\n    </span>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/kinetic-center-build/index.tsx","type":"registry:ui"}],"name":"kinetic-center-build","registryDependencies":[],"title":"Kinetic Center Build","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"A LineByLineSlide text animation component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { motion, useInView, useReducedMotion } from \"motion/react\";\nimport { useRef } from \"react\";\n\nexport interface LineByLineSlideProps {\n  /** Text content. Use \"\\n\" to separate lines, or pass `lines` instead. */\n  children?: string;\n  className?: string;\n  /** Delay before the animation starts, in milliseconds. */\n  delay?: number;\n  /** Explicit line array. Takes precedence over children. */\n  lines?: string[];\n  /** Per-line stagger, in milliseconds. */\n  stagger?: number;\n  /** Animate only once the text scrolls into view. */\n  triggerOnView?: boolean;\n}\n\nconst DURATION_S = 0.9;\nconst MS = 1000;\nconst EASE = [0.22, 1, 0.36, 1] as const;\n\n/**\n * LineByLineSlide — per-line slide-in from the left with a staggered entrance,\n * inspired by Apple landing page subheads. Each line enters from the left for a\n * flowing paragraph reveal. From the animate-text catalog (`line-by-line-slide`).\n * Best for 2-line or 3-line headings.\n */\nexport default function LineByLineSlide({\n  children,\n  lines: linesProp,\n  className = \"\",\n  delay = 0,\n  stagger = 120,\n  triggerOnView = false,\n}: LineByLineSlideProps) {\n  const ref = useRef<HTMLSpanElement>(null);\n  const inView = useInView(ref, { once: true });\n  const shouldReduceMotion = useReducedMotion();\n  const play = (!triggerOnView || inView) && !shouldReduceMotion;\n\n  const lines = linesProp ?? (children ? children.split(\"\\n\") : []);\n  const label = lines.join(\" \");\n\n  return (\n    <span\n      aria-label={label}\n      className={className}\n      ref={ref}\n      style={{ display: \"block\" }}\n    >\n      {lines.map((line, index) => (\n        <motion.span\n          animate={play ? { opacity: 1, x: 0 } : undefined}\n          aria-hidden=\"true\"\n          initial={shouldReduceMotion ? { opacity: 1 } : { opacity: 0, x: -48 }}\n          // biome-ignore lint/suspicious/noArrayIndexKey: lines have no stable id\n          key={index}\n          style={{ display: \"block\" }}\n          transition={\n            shouldReduceMotion\n              ? { duration: 0 }\n              : {\n                  delay: delay / MS + (index * stagger) / MS,\n                  duration: DURATION_S,\n                  ease: EASE,\n                }\n          }\n        >\n          {line}\n        </motion.span>\n      ))}\n    </span>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/line-by-line-slide/index.tsx","type":"registry:ui"}],"name":"line-by-line-slide","registryDependencies":[],"title":"Line By Line Slide","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["@radix-ui/react-slot","class-variance-authority","motion"],"description":"Button that subtly follows the cursor with magnetic effect","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cn } from \"@/lib/utils\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { motion, useReducedMotion, useSpring } from \"motion/react\";\nimport type { ButtonHTMLAttributes, ReactNode } from \"react\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\n\nconst magneticButtonVariants = cva(\n  \"inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md font-medium text-sm ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50\",\n  {\n    defaultVariants: {\n      size: \"default\",\n      variant: \"default\",\n    },\n    variants: {\n      size: {\n        default: \"h-10 px-4 py-2\",\n        icon: \"h-10 w-10\",\n        lg: \"h-11 rounded-md px-8\",\n        sm: \"h-9 rounded-md px-3\",\n      },\n      variant: {\n        default: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n        destructive:\n          \"bg-destructive text-destructive-foreground hover:bg-destructive/90\",\n        ghost: \"hover:bg-accent hover:text-accent-foreground\",\n        link: \"text-foreground underline-offset-4 hover:underline\",\n        outline:\n          \"border border-input bg-background hover:bg-accent hover:text-accent-foreground\",\n        secondary:\n          \"bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n      },\n    },\n  }\n);\n\nexport type MagneticButtonProps = {\n  children: ReactNode;\n  strength?: number;\n  radius?: number;\n  springConfig?: { duration?: number; bounce?: number };\n  disabled?: boolean;\n  asChild?: boolean;\n  className?: string;\n} & ButtonHTMLAttributes<HTMLButtonElement> &\n  VariantProps<typeof magneticButtonVariants>;\n\nconst MagneticButton = ({\n  children,\n  strength = 0.3,\n  radius = 150,\n  springConfig = { bounce: 0.1, duration: 0.4 },\n  disabled = false,\n  asChild = false,\n  variant,\n  size,\n  className,\n  ...props\n}: MagneticButtonProps) => {\n  const shouldReduceMotion = useReducedMotion();\n  const [isHoverDevice, setIsHoverDevice] = useState(false);\n  const buttonRef = useRef<HTMLButtonElement>(null);\n  const wrapperRef = useRef<HTMLDivElement>(null);\n\n  const x = useSpring(0, {\n    bounce: springConfig.bounce ?? 0.1,\n    duration: springConfig.duration ?? 0.4,\n  });\n  const y = useSpring(0, {\n    bounce: springConfig.bounce ?? 0.1,\n    duration: springConfig.duration ?? 0.4,\n  });\n\n  useEffect(() => {\n    const mediaQuery = window.matchMedia(\"(hover: hover) and (pointer: fine)\");\n    setIsHoverDevice(mediaQuery.matches);\n\n    const handleChange = (e: MediaQueryListEvent) => {\n      setIsHoverDevice(e.matches);\n    };\n\n    mediaQuery.addEventListener(\"change\", handleChange);\n    return () => mediaQuery.removeEventListener(\"change\", handleChange);\n  }, []);\n\n  const isEffectDisabled = disabled || shouldReduceMotion || !isHoverDevice;\n\n  const handleMouseMove = useCallback(\n    (event: React.MouseEvent<HTMLDivElement>) => {\n      if (isEffectDisabled || !buttonRef.current) {\n        return;\n      }\n\n      const rect = buttonRef.current.getBoundingClientRect();\n      const centerX = rect.left + rect.width / 2;\n      const centerY = rect.top + rect.height / 2;\n\n      const distanceX = event.clientX - centerX;\n      const distanceY = event.clientY - centerY;\n      const distance = Math.sqrt(distanceX * distanceX + distanceY * distanceY);\n\n      if (distance < radius) {\n        const factor = 1 - distance / radius;\n        const moveX = distanceX * strength * factor;\n        const moveY = distanceY * strength * factor;\n        x.set(moveX);\n        y.set(moveY);\n      } else {\n        x.set(0);\n        y.set(0);\n      }\n    },\n    [isEffectDisabled, radius, strength, x, y]\n  );\n\n  const handleMouseLeave = useCallback(() => {\n    x.set(0);\n    y.set(0);\n  }, [x, y]);\n\n  const Comp = asChild ? Slot : \"button\";\n\n  return (\n    // biome-ignore lint/a11y/noStaticElementInteractions: Mouse events are for visual effect, not interaction\n    <div\n      className=\"inline-block\"\n      onMouseLeave={handleMouseLeave}\n      onMouseMove={handleMouseMove}\n      ref={wrapperRef}\n      role=\"presentation\"\n      style={{\n        margin: `-${radius / 2}px`,\n        padding: `${radius / 2}px`,\n      }}\n    >\n      <motion.div style={{ x, y }}>\n        <Comp\n          className={cn(magneticButtonVariants({ className, size, variant }))}\n          disabled={disabled}\n          ref={buttonRef}\n          type=\"button\"\n          {...props}\n        >\n          {children}\n        </Comp>\n      </motion.div>\n    </div>\n  );\n};\n\nexport default MagneticButton;\n","path":"index.tsx","target":"components/smoothui/magnetic-button/index.tsx","type":"registry:ui"}],"name":"magnetic-button","registryDependencies":[],"title":"Magnetic Button","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"A MaskRevealUp text animation component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { motion, useInView, useReducedMotion } from \"motion/react\";\nimport { useRef } from \"react\";\n\nexport interface MaskRevealUpProps {\n  /** Text content. Use \"\\n\" to separate lines, or pass `lines` instead. */\n  children?: string;\n  className?: string;\n  /** Delay before the animation starts, in milliseconds. */\n  delay?: number;\n  /** Explicit line array. Takes precedence over children. */\n  lines?: string[];\n  /** Per-line stagger, in milliseconds. */\n  stagger?: number;\n  /** Animate only once the text scrolls into view. */\n  triggerOnView?: boolean;\n}\n\nconst DURATION_S = 0.76;\nconst MS = 1000;\nconst EASE = [0.22, 1, 0.36, 1] as const;\n\n/**\n * MaskRevealUp — per-line masked reveal with upward motion and soft blur,\n * inspired by Apple section transitions. Each line rises from beneath an\n * overflow-hidden mask. From the animate-text catalog (`mask-reveal-up`).\n * Best for two-line and three-line headings.\n */\nexport default function MaskRevealUp({\n  children,\n  lines: linesProp,\n  className = \"\",\n  delay = 0,\n  stagger = 90,\n  triggerOnView = false,\n}: MaskRevealUpProps) {\n  const ref = useRef<HTMLSpanElement>(null);\n  const inView = useInView(ref, { once: true });\n  const shouldReduceMotion = useReducedMotion();\n  const play = (!triggerOnView || inView) && !shouldReduceMotion;\n\n  const lines = linesProp ?? (children ? children.split(\"\\n\") : []);\n  const label = lines.join(\" \");\n\n  return (\n    <span\n      aria-label={label}\n      className={className}\n      ref={ref}\n      style={{ display: \"block\" }}\n    >\n      {lines.map((line, index) => (\n        // biome-ignore lint/suspicious/noArrayIndexKey: lines have no stable id\n        <span key={index} style={{ display: \"block\", overflow: \"hidden\" }}>\n          <motion.span\n            animate={\n              play ? { filter: \"blur(0px)\", opacity: 1, y: 0 } : undefined\n            }\n            aria-hidden=\"true\"\n            initial={\n              shouldReduceMotion\n                ? { opacity: 1 }\n                : { filter: \"blur(6px)\", opacity: 0, y: 30 }\n            }\n            style={{ display: \"block\" }}\n            transition={\n              shouldReduceMotion\n                ? { duration: 0 }\n                : {\n                    delay: delay / MS + (index * stagger) / MS,\n                    duration: DURATION_S,\n                    ease: EASE,\n                  }\n            }\n          >\n            {line}\n          </motion.span>\n        </span>\n      ))}\n    </span>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/mask-reveal-up/index.tsx","type":"registry:ui"}],"name":"mask-reveal-up","registryDependencies":[],"title":"Mask Reveal Up","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"A MicroScaleFade text animation component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { motion, useInView, useReducedMotion } from \"motion/react\";\nimport { useRef } from \"react\";\n\nexport interface MicroScaleFadeProps {\n  children: string;\n  className?: string;\n  /** Delay before the animation starts, in milliseconds. */\n  delay?: number;\n  /** Animate only once the text scrolls into view. */\n  triggerOnView?: boolean;\n}\n\nconst DURATION_S = 0.6;\nconst MS = 1000;\nconst EASE = [0.32, 0.72, 0, 1] as const;\n\n/**\n * MicroScaleFade — calm, tiny scale pop used as subtle premium polish for\n * labels and headings. The whole text animates as a single element from\n * scale 0.96 to 1. From the animate-text catalog (`micro-scale-fade`).\n * Best for single words or short titles.\n */\nexport default function MicroScaleFade({\n  children,\n  className = \"\",\n  delay = 0,\n  triggerOnView = false,\n}: MicroScaleFadeProps) {\n  const ref = useRef<HTMLSpanElement>(null);\n  const inView = useInView(ref, { once: true });\n  const shouldReduceMotion = useReducedMotion();\n  const play = (!triggerOnView || inView) && !shouldReduceMotion;\n\n  return (\n    <motion.span\n      animate={play ? { opacity: 1, scale: 1 } : undefined}\n      aria-label={children}\n      className={className}\n      initial={\n        shouldReduceMotion ? { opacity: 1 } : { opacity: 0, scale: 0.96 }\n      }\n      ref={ref}\n      style={{ display: \"inline-block\" }}\n      transition={\n        shouldReduceMotion\n          ? { duration: 0 }\n          : {\n              delay: delay / MS,\n              duration: DURATION_S,\n              ease: EASE,\n            }\n      }\n    >\n      {children}\n    </motion.span>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/micro-scale-fade/index.tsx","type":"registry:ui"}],"name":"micro-scale-fade","registryDependencies":[],"title":"Micro Scale Fade","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["class-variance-authority","motion"],"description":"Feedback dock that morphs from a compact pill into a panel and back, with a spring layout transition.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport SmoothButton from \"@/components/smoothui/smooth-button\";\nimport { cx } from \"class-variance-authority\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport React from \"react\";\nimport SiriOrb from \"../siri-orb\";\nimport { useClickOutside } from \"./use-click-outside\";\n\nconst SPEED = 1;\nconst SUCCESS_DURATION = 1500;\nconst DOCK_HEIGHT = 44;\nconst FEEDBACK_BORDER_RADIUS = 14;\nconst DOCK_BORDER_RADIUS = 20;\nconst SPRING_STIFFNESS = 550;\nconst SPRING_DAMPING = 45;\nconst SPRING_MASS = 0.7;\nconst CLOSE_DELAY = 0.08;\n\ninterface FooterContext {\n  closeFeedback: () => void;\n  openFeedback: () => void;\n  showFeedback: boolean;\n  success: boolean;\n}\n\nconst FooterContext = React.createContext({} as FooterContext);\nconst useFooter = () => React.useContext(FooterContext);\n\nexport function MorphSurface() {\n  const rootRef = React.useRef<HTMLDivElement>(null);\n\n  const feedbackRef = React.useRef<HTMLTextAreaElement | null>(null);\n  const [showFeedback, setShowFeedback] = React.useState(false);\n  const [success, setSuccess] = React.useState(false);\n  const shouldReduceMotion = useReducedMotion();\n\n  const closeFeedback = React.useCallback(() => {\n    setShowFeedback(false);\n    feedbackRef.current?.blur();\n  }, []);\n\n  const openFeedback = React.useCallback(() => {\n    setShowFeedback(true);\n    setTimeout(() => {\n      feedbackRef.current?.focus();\n    });\n  }, []);\n\n  const onFeedbackSuccess = React.useCallback(() => {\n    closeFeedback();\n    setSuccess(true);\n    setTimeout(() => {\n      setSuccess(false);\n    }, SUCCESS_DURATION);\n  }, [closeFeedback]);\n\n  useClickOutside(rootRef, closeFeedback);\n\n  const context = React.useMemo(\n    () => ({\n      closeFeedback,\n      openFeedback,\n      showFeedback,\n      success,\n    }),\n    [showFeedback, success, openFeedback, closeFeedback]\n  );\n\n  return (\n    <div\n      className=\"flex items-center justify-center\"\n      style={{\n        height: FEEDBACK_HEIGHT,\n        width: FEEDBACK_WIDTH,\n      }}\n    >\n      <motion.div\n        animate={\n          shouldReduceMotion\n            ? {}\n            : {\n                borderRadius: showFeedback\n                  ? FEEDBACK_BORDER_RADIUS\n                  : DOCK_BORDER_RADIUS,\n                height: showFeedback ? FEEDBACK_HEIGHT : DOCK_HEIGHT,\n                width: showFeedback ? FEEDBACK_WIDTH : \"auto\",\n              }\n        }\n        className={cx(\n          \"relative bottom-8 z-3 flex flex-col items-center overflow-hidden border bg-background max-sm:bottom-5\"\n        )}\n        data-footer\n        initial={false}\n        ref={rootRef}\n        transition={\n          shouldReduceMotion\n            ? { duration: 0 }\n            : {\n                damping: SPRING_DAMPING,\n                delay: showFeedback ? 0 : CLOSE_DELAY,\n                duration: 0.25,\n                mass: SPRING_MASS,\n                stiffness: SPRING_STIFFNESS / SPEED,\n                type: \"spring\" as const,\n              }\n        }\n      >\n        <FooterContext.Provider value={context}>\n          <Dock />\n          <Feedback onSuccess={onFeedbackSuccess} ref={feedbackRef} />\n        </FooterContext.Provider>\n      </motion.div>\n    </div>\n  );\n}\n\nfunction Dock() {\n  const { showFeedback, openFeedback } = useFooter();\n  const shouldReduceMotion = useReducedMotion();\n  return (\n    <footer className=\"mt-auto flex h-[44px] select-none items-center justify-center whitespace-nowrap\">\n      <div className=\"flex items-center justify-center gap-2 px-3 max-sm:h-10 max-sm:px-2\">\n        <div className=\"flex w-fit items-center gap-2\">\n          <AnimatePresence mode=\"wait\">\n            {showFeedback ? (\n              <motion.div\n                animate={shouldReduceMotion ? {} : { opacity: 0 }}\n                className=\"h-5 w-5\"\n                exit={shouldReduceMotion ? {} : { opacity: 0 }}\n                initial={shouldReduceMotion ? {} : { opacity: 0 }}\n                key=\"placeholder\"\n                transition={\n                  shouldReduceMotion ? { duration: 0 } : { duration: 0.2 }\n                }\n              />\n            ) : (\n              <motion.div\n                animate={shouldReduceMotion ? { opacity: 1 } : { opacity: 1 }}\n                exit={\n                  shouldReduceMotion\n                    ? { opacity: 0, transition: { duration: 0 } }\n                    : { opacity: 0 }\n                }\n                initial={shouldReduceMotion ? { opacity: 1 } : { opacity: 0 }}\n                key=\"siri-orb\"\n                transition={\n                  shouldReduceMotion ? { duration: 0 } : { duration: 0.2 }\n                }\n              >\n                <SiriOrb\n                  colors={{\n                    bg: \"oklch(22.64% 0 0)\",\n                  }}\n                  size=\"24px\"\n                />\n              </motion.div>\n            )}\n          </AnimatePresence>\n        </div>\n\n        <SmoothButton\n          className=\"flex h-fit flex-1 justify-end rounded-full px-2 py-0.5!\"\n          onClick={openFeedback}\n          type=\"button\"\n          variant=\"ghost\"\n        >\n          <span className=\"truncate\">Ask AI</span>\n        </SmoothButton>\n      </div>\n    </footer>\n  );\n}\n\nconst FEEDBACK_WIDTH = 360;\nconst FEEDBACK_HEIGHT = 200;\n\nfunction Feedback({\n  ref,\n  onSuccess,\n}: {\n  ref: React.Ref<HTMLTextAreaElement>;\n  onSuccess: () => void;\n}) {\n  const { closeFeedback, showFeedback } = useFooter();\n  const shouldReduceMotion = useReducedMotion();\n  const submitRef = React.useRef<HTMLButtonElement>(null);\n\n  function onSubmit(e: React.FormEvent<HTMLFormElement>) {\n    e.preventDefault();\n    onSuccess();\n  }\n\n  function onKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>) {\n    if (e.key === \"Escape\") {\n      closeFeedback();\n    }\n    if (e.key === \"Enter\" && e.metaKey) {\n      e.preventDefault();\n      submitRef.current?.click();\n    }\n  }\n\n  return (\n    <form\n      className=\"absolute bottom-0\"\n      onSubmit={onSubmit}\n      style={{\n        height: FEEDBACK_HEIGHT,\n        pointerEvents: showFeedback ? \"all\" : \"none\",\n        width: FEEDBACK_WIDTH,\n      }}\n    >\n      <AnimatePresence>\n        {showFeedback ? (\n          <motion.div\n            animate={shouldReduceMotion ? { opacity: 1 } : { opacity: 1 }}\n            className=\"flex h-full flex-col p-1\"\n            exit={\n              shouldReduceMotion\n                ? { opacity: 0, transition: { duration: 0 } }\n                : { opacity: 0 }\n            }\n            initial={shouldReduceMotion ? { opacity: 1 } : { opacity: 0 }}\n            transition={\n              shouldReduceMotion\n                ? { duration: 0 }\n                : {\n                    damping: SPRING_DAMPING,\n                    duration: 0.25,\n                    mass: SPRING_MASS,\n                    stiffness: SPRING_STIFFNESS / SPEED,\n                    type: \"spring\" as const,\n                  }\n            }\n          >\n            <div className=\"flex justify-between py-1\">\n              <p className=\"z-2 ml-[38px] flex select-none items-center gap-[6px] text-foreground\">\n                AI Input\n              </p>\n              <button\n                className=\"right-4 mt-1 flex -translate-y-[3px] cursor-pointer select-none items-center justify-center gap-1 rounded-[12px] bg-transparent pr-1 text-center text-foreground\"\n                ref={submitRef}\n                type=\"submit\"\n              >\n                <Kbd>⌘</Kbd>\n                <Kbd className=\"w-fit\">Enter</Kbd>\n              </button>\n            </div>\n            <textarea\n              className=\"h-full w-full resize-none scroll-py-2 rounded-md bg-primary p-4 outline-0\"\n              name=\"message\"\n              onKeyDown={onKeyDown}\n              placeholder=\"Ask me anything...\"\n              ref={ref}\n              required\n              spellCheck={false}\n            />\n          </motion.div>\n        ) : null}\n      </AnimatePresence>\n      <AnimatePresence>\n        {showFeedback ? (\n          <motion.div\n            animate={shouldReduceMotion ? { opacity: 1 } : { opacity: 1 }}\n            className=\"absolute top-2 left-3\"\n            exit={\n              shouldReduceMotion\n                ? { opacity: 0, transition: { duration: 0 } }\n                : { opacity: 0 }\n            }\n            initial={shouldReduceMotion ? { opacity: 1 } : { opacity: 0 }}\n            transition={\n              shouldReduceMotion ? { duration: 0 } : { duration: 0.2 }\n            }\n          >\n            <SiriOrb\n              colors={{\n                bg: \"oklch(22.64% 0 0)\",\n              }}\n              size=\"24px\"\n            />\n          </motion.div>\n        ) : null}\n      </AnimatePresence>\n    </form>\n  );\n}\n\nfunction Kbd({\n  children,\n  className,\n}: {\n  children: string;\n  className?: string;\n}) {\n  return (\n    <kbd\n      className={cx(\n        \"flex h-6 w-fit items-center justify-center rounded-sm border bg-primary px-[6px] font-sans text-foreground\",\n        className\n      )}\n    >\n      {children}\n    </kbd>\n  );\n}\n\n// Add default export for lazy loading\nexport default MorphSurface;\n","path":"index.tsx","target":"components/smoothui/morph-surface/index.tsx","type":"registry:ui"},{"content":"\"use client\";\n\nimport { type RefObject, useEffect } from \"react\";\n\ntype EventType =\n  | \"mousedown\"\n  | \"mouseup\"\n  | \"touchstart\"\n  | \"touchend\"\n  | \"focusin\"\n  | \"focusout\";\n\nexport function useClickOutside<T extends HTMLElement = HTMLElement>(\n  ref: RefObject<T | null> | RefObject<T | null>[],\n  handler: (event: Event) => void,\n  eventType: EventType = \"mousedown\"\n): void {\n  useEffect(() => {\n    function callback(event: Event) {\n      const target = event.target as Node;\n\n      // Do nothing if the target is not connected element with document\n      if (!target?.isConnected) {\n        return;\n      }\n\n      const isOutside = Array.isArray(ref)\n        ? ref\n            .filter((r) => Boolean(r.current))\n            .every((r) => r.current && !r.current.contains(target))\n        : ref.current && !ref.current.contains(target);\n\n      if (isOutside) {\n        handler(event);\n      }\n    }\n\n    window.addEventListener(eventType, callback);\n\n    return () => {\n      window.removeEventListener(eventType, callback);\n    };\n  }, [eventType, handler, ref]);\n}\n","path":"use-click-outside.tsx","target":"components/smoothui/morph-surface/use-click-outside.tsx","type":"registry:ui"}],"name":"morph-surface","registryDependencies":["https://smoothui.dev/r/smooth-button.json","https://smoothui.dev/r/siri-orb.json"],"title":"Morph Surface","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"Animated notification badge with count and status variants","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport type { ReactNode } from \"react\";\nimport { useEffect, useRef, useState } from \"react\";\n\nexport interface NotificationBadgeProps {\n  children?: ReactNode;\n  className?: string;\n  count?: number;\n  max?: number;\n  ping?: boolean;\n  position?: \"top-right\" | \"top-left\" | \"bottom-right\" | \"bottom-left\";\n  showZero?: boolean;\n  status?: \"online\" | \"offline\" | \"busy\" | \"away\";\n  variant?: \"dot\" | \"count\" | \"status\";\n}\n\nconst statusColors = {\n  away: \"bg-amber-500\",\n  busy: \"bg-red-500\",\n  offline: \"bg-gray-400\",\n  online: \"bg-emerald-500\",\n};\n\nconst positionClasses = {\n  \"bottom-left\": \"-bottom-1 -left-1\",\n  \"bottom-right\": \"-bottom-1 -right-1\",\n  \"top-left\": \"-top-1 -left-1\",\n  \"top-right\": \"-top-1 -right-1\",\n};\n\nconst AnimatedCount = ({\n  value,\n  max,\n  shouldReduceMotion,\n}: {\n  value: number;\n  max: number;\n  shouldReduceMotion: boolean | null;\n}) => {\n  const displayValue = value > max ? `${max}+` : value.toString();\n  const prevValueRef = useRef(value);\n  const direction = value > prevValueRef.current ? 1 : -1;\n\n  useEffect(() => {\n    prevValueRef.current = value;\n  }, [value]);\n\n  if (shouldReduceMotion) {\n    return <span className=\"font-medium leading-none\">{displayValue}</span>;\n  }\n\n  return (\n    <span className=\"relative overflow-hidden font-medium leading-none\">\n      <AnimatePresence initial={false} mode=\"popLayout\">\n        <motion.span\n          animate={{ opacity: 1, y: 0 }}\n          className=\"inline-block\"\n          exit={{ opacity: 0, y: direction * -12 }}\n          initial={{ opacity: 0, y: direction * 12 }}\n          key={value}\n          transition={{ bounce: 0.1, duration: 0.3, type: \"spring\" as const }}\n        >\n          {displayValue}\n        </motion.span>\n      </AnimatePresence>\n    </span>\n  );\n};\n\nconst NotificationBadge = ({\n  variant = \"dot\",\n  count = 0,\n  max = 99,\n  status = \"online\",\n  showZero = false,\n  ping = false,\n  position = \"top-right\",\n  children,\n  className,\n}: NotificationBadgeProps) => {\n  const shouldReduceMotion = useReducedMotion();\n  const [isVisible, setIsVisible] = useState(false);\n\n  useEffect(() => {\n    const shouldShow =\n      variant === \"dot\" ||\n      variant === \"status\" ||\n      (variant === \"count\" && (count > 0 || showZero));\n    setIsVisible(shouldShow);\n  }, [variant, count, showZero]);\n\n  const getBadgeClasses = () => {\n    if (variant === \"dot\") {\n      return \"h-2.5 w-2.5\";\n    }\n    if (variant === \"status\") {\n      return \"h-3 w-3\";\n    }\n    const displayValue = count > max ? `${max}+` : count.toString();\n    if (displayValue.length === 1) {\n      return \"h-5 w-5 text-xs\";\n    }\n    if (displayValue.length === 2) {\n      return \"h-5 min-w-5 px-1 text-xs\";\n    }\n    return \"h-5 min-w-6 px-1 text-xs\";\n  };\n\n  const getBackgroundColor = () => {\n    if (variant === \"status\") {\n      return statusColors[status];\n    }\n    return \"bg-brand\";\n  };\n\n  const badgeElement = (\n    <AnimatePresence mode=\"wait\">\n      {isVisible ? (\n        <motion.span\n          animate={{ opacity: 1, scale: 1 }}\n          className={cn(\n            \"absolute flex items-center justify-center rounded-full text-white\",\n            getBackgroundColor(),\n            getBadgeClasses(),\n            positionClasses[position],\n            variant === \"status\" && \"ring-2 ring-white dark:ring-gray-900\",\n            className\n          )}\n          exit={\n            shouldReduceMotion\n              ? { opacity: 0, transition: { duration: 0 } }\n              : { opacity: 0, scale: 0, transition: { duration: 0.15 } }\n          }\n          initial={\n            shouldReduceMotion ? { opacity: 1 } : { opacity: 0, scale: 0 }\n          }\n          transition={\n            shouldReduceMotion\n              ? { duration: 0 }\n              : { bounce: 0.2, duration: 0.25, type: \"spring\" as const }\n          }\n        >\n          {variant === \"count\" && (\n            <AnimatedCount\n              max={max}\n              shouldReduceMotion={shouldReduceMotion}\n              value={count}\n            />\n          )}\n\n          {ping && !shouldReduceMotion && (\n            <span\n              aria-hidden=\"true\"\n              className={cn(\n                \"absolute inset-0 animate-ping rounded-full opacity-75\",\n                getBackgroundColor()\n              )}\n            />\n          )}\n        </motion.span>\n      ) : null}\n    </AnimatePresence>\n  );\n\n  if (!children) {\n    return (\n      <span className=\"relative inline-flex\">\n        <span className=\"h-4 w-4\" />\n        {badgeElement}\n      </span>\n    );\n  }\n\n  return (\n    <span className=\"relative inline-flex\">\n      {children}\n      {badgeElement}\n    </span>\n  );\n};\n\nexport default NotificationBadge;\n","path":"index.tsx","target":"components/smoothui/notification-badge/index.tsx","type":"registry:ui"}],"name":"notification-badge","registryDependencies":["https://smoothui.dev/r/tokens.json"],"title":"Notification Badge","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["lucide-react"],"description":"A NumberFlow component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { Minus, Plus } from \"lucide-react\";\nimport { useEffect, useRef, useState } from \"react\";\n\nconst TENS_PLACE = 10;\nconst HUNDREDS_PLACE = 100;\n\nconst animateDigit = (\n  prevElement: HTMLElement | null,\n  nextElement: HTMLElement | null,\n  isIncreasing: boolean\n) => {\n  if (!prevElement) {\n    return;\n  }\n  if (!nextElement) {\n    return;\n  }\n\n  if (isIncreasing) {\n    prevElement.classList.add(\"slide-out-up\");\n    nextElement.classList.add(\"slide-in-up\");\n  } else {\n    prevElement.classList.add(\"slide-out-down\");\n    nextElement.classList.add(\"slide-in-down\");\n  }\n\n  const handleAnimationEnd = () => {\n    prevElement.classList.remove(\"slide-out-up\", \"slide-out-down\");\n    nextElement.classList.remove(\"slide-in-up\", \"slide-in-down\");\n    prevElement.removeEventListener(\"animationend\", handleAnimationEnd);\n  };\n\n  prevElement.addEventListener(\"animationend\", handleAnimationEnd);\n};\n\nconst getTensValue = (num: number) => Math.floor(num / TENS_PLACE);\nconst getHundredsValue = (num: number) => Math.floor(num / HUNDREDS_PLACE);\n\nexport interface NumberFlowProps {\n  buttonClassName?: string;\n  className?: string;\n  digitClassName?: string;\n  max?: number;\n  min?: number;\n  onChange?: (value: number) => void;\n  value?: number;\n}\n\nexport default function NumberFlow({\n  value: controlledValue,\n  onChange,\n  min = 0,\n  max = 999,\n  className = \"\",\n  digitClassName = \"\",\n  buttonClassName = \"\",\n}: NumberFlowProps) {\n  const [internalValue, setInternalValue] = useState(0);\n  const [prevValue, setPrevValue] = useState(0);\n\n  const value = controlledValue === undefined ? internalValue : controlledValue;\n\n  const prevValueRef = useRef<HTMLElement>(null);\n  const nextValueRef = useRef<HTMLElement>(null);\n  const prevValueTens = useRef<HTMLElement>(null);\n  const nextValueTens = useRef<HTMLElement>(null);\n  const prevValueHunds = useRef<HTMLElement>(null);\n  const nextValueHunds = useRef<HTMLElement>(null);\n\n  const setValue = (val: number) => {\n    if (onChange) {\n      onChange(val);\n    } else {\n      setInternalValue(val);\n    }\n  };\n\n  const add = () => {\n    if (value < max) {\n      setPrevValue(value);\n      setValue(value + 1);\n    }\n  };\n\n  const subtract = () => {\n    if (value > min) {\n      setPrevValue(value);\n      setValue(value - 1);\n    }\n  };\n\n  useEffect(() => {\n    if (prevValueRef.current && nextValueRef.current) {\n      animateDigit(\n        prevValueRef.current,\n        nextValueRef.current,\n        value > prevValue\n      );\n    }\n\n    const currentTens = getTensValue(value);\n    const prevTens = getTensValue(prevValue);\n\n    if (\n      prevValueTens.current &&\n      nextValueTens.current &&\n      currentTens !== prevTens\n    ) {\n      animateDigit(\n        prevValueTens.current,\n        nextValueTens.current,\n        currentTens > prevTens\n      );\n    }\n\n    const currentHundreds = getHundredsValue(value);\n    const prevHundreds = getHundredsValue(prevValue);\n\n    if (\n      prevValueHunds.current &&\n      nextValueHunds.current &&\n      currentHundreds !== prevHundreds\n    ) {\n      animateDigit(\n        prevValueHunds.current,\n        nextValueHunds.current,\n        currentHundreds > prevHundreds\n      );\n    }\n  }, [value, prevValue]);\n\n  return (\n    <div\n      className={cn(\n        \"flex flex-col items-center justify-center gap-8\",\n        className\n      )}\n    >\n      <div className=\"flex items-center gap-2 rounded-xl border bg-background p-4\">\n        <div className={cn(\"flex items-center gap-1\", digitClassName)}>\n          <div\n            className={cn(\n              \"relative h-16 w-12 overflow-hidden rounded-lg border bg-primary\"\n            )}\n          >\n            <span\n              className=\"absolute inset-0 flex items-center justify-center font-semibold text-2xl text-foreground\"\n              ref={prevValueHunds}\n              style={{ transform: \"translateY(-100%)\" }}\n            >\n              {Math.floor(prevValue / HUNDREDS_PLACE)}\n            </span>\n            <span\n              className=\"absolute inset-0 flex items-center justify-center font-semibold text-2xl text-foreground\"\n              ref={nextValueHunds}\n              style={{ transform: \"translateY(0%)\" }}\n            >\n              {Math.floor(value / HUNDREDS_PLACE)}\n            </span>\n          </div>\n          <div\n            className={cn(\n              \"relative h-16 w-12 overflow-hidden rounded-lg border bg-primary\"\n            )}\n          >\n            <span\n              className=\"absolute inset-0 flex items-center justify-center font-semibold text-2xl text-foreground\"\n              ref={prevValueTens}\n              style={{ transform: \"translateY(-100%)\" }}\n            >\n              {Math.floor(prevValue / TENS_PLACE) % TENS_PLACE}\n            </span>\n            <span\n              className=\"absolute inset-0 flex items-center justify-center font-semibold text-2xl text-foreground\"\n              ref={nextValueTens}\n              style={{ transform: \"translateY(0%)\" }}\n            >\n              {Math.floor(value / TENS_PLACE) % TENS_PLACE}\n            </span>\n          </div>\n          <div\n            className={cn(\n              \"relative h-16 w-12 overflow-hidden rounded-lg border bg-primary\"\n            )}\n          >\n            <span\n              className=\"absolute inset-0 flex items-center justify-center font-semibold text-2xl text-foreground\"\n              ref={prevValueRef}\n              style={{ transform: \"translateY(-100%)\" }}\n            >\n              {prevValue % TENS_PLACE}\n            </span>\n            <span\n              className=\"absolute inset-0 flex items-center justify-center font-semibold text-2xl text-foreground\"\n              ref={nextValueRef}\n              style={{ transform: \"translateY(0%)\" }}\n            >\n              {value % TENS_PLACE}\n            </span>\n          </div>\n        </div>\n\n        <div className=\"flex flex-col gap-1\">\n          <button\n            aria-label=\"Increase number\"\n            className={cn(\n              \"relative w-auto cursor-pointer overflow-hidden rounded-md border bg-background p-2 disabled:cursor-not-allowed disabled:opacity-50\",\n              buttonClassName\n            )}\n            disabled={value >= max}\n            onClick={add}\n            type=\"button\"\n          >\n            <Plus className=\"h-3 w-3\" />\n          </button>\n          <button\n            aria-label=\"Decrease number\"\n            className={cn(\n              \"relative w-auto cursor-pointer overflow-hidden rounded-md border bg-background p-2 disabled:cursor-not-allowed disabled:opacity-50\",\n              buttonClassName\n            )}\n            disabled={value <= min}\n            onClick={subtract}\n            type=\"button\"\n          >\n            <Minus className=\"h-3 w-3\" />\n          </button>\n        </div>\n      </div>\n    </div>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/number-flow/index.tsx","type":"registry:ui"}],"name":"number-flow","registryDependencies":[],"title":"Number Flow","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":[],"description":"A soft-min SDF merge transition with two organic expanding shapes.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport SdfBlobTransition from \"../sdf-blob-transition\";\n\nexport interface OrganicMergeTransitionProps {\n  children: ReactNode;\n  className?: string;\n  duration?: number;\n  onRest?: () => void;\n  transitionKey: string | number;\n}\n\nconst OrganicMergeTransition = ({\n  children,\n  className,\n  duration = 1120,\n  onRest,\n  transitionKey,\n}: OrganicMergeTransitionProps) => (\n  <SdfBlobTransition\n    className={className}\n    duration={duration}\n    onRest={onRest}\n    transitionKey={transitionKey}\n    variant=\"merge\"\n  >\n    {children}\n  </SdfBlobTransition>\n);\n\nexport default OrganicMergeTransition;\n","path":"index.tsx","target":"components/smoothui/organic-merge-transition/index.tsx","type":"registry:ui"}],"name":"organic-merge-transition","registryDependencies":["https://smoothui.dev/r/sdf-blob-transition.json"],"title":"Organic Merge Transition","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion","lucide-react"],"description":"Animated pagination with spring-based active page indicator","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { ChevronLeft, ChevronRight } from \"lucide-react\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { useCallback, useId, useMemo } from \"react\";\nimport { SPRING_DEFAULT } from \"@/components/smoothui/lib/animation\";\nimport SmoothButton from \"../smooth-button\";\n\nexport type PaginationProps = {\n  /** Current active page (1-indexed). */\n  page: number;\n  /** Total number of pages. */\n  totalPages: number;\n  /** Called when the user selects a different page. */\n  onPageChange: (page: number) => void;\n  /** Number of sibling pages to show on each side of current page. Defaults to 1. */\n  siblings?: number;\n  /** Additional CSS classes for the nav element. */\n  className?: string;\n};\n\nconst ELLIPSIS = \"ellipsis\" as const;\n\ntype PageItem = number | typeof ELLIPSIS;\n\n/** Spring for the sliding active indicator (like animated-tabs) */\nconst SPRING_INDICATOR = {\n  bounce: 0.05,\n  duration: 0.25,\n  type: \"spring\" as const,\n};\n\n/** Stagger delay per page button on initial render */\nconst STAGGER_DELAY = 0.03;\n\nconst buildPageRange = (\n  page: number,\n  totalPages: number,\n  siblings: number\n): PageItem[] => {\n  const totalSlots = siblings * 2 + 5; // siblings + current + 2 boundary + 2 potential ellipsis\n\n  if (totalPages <= totalSlots) {\n    return Array.from({ length: totalPages }, (_, i) => i + 1);\n  }\n\n  const leftSibling = Math.max(page - siblings, 2);\n  const rightSibling = Math.min(page + siblings, totalPages - 1);\n\n  const showLeftEllipsis = leftSibling > 2;\n  const showRightEllipsis = rightSibling < totalPages - 1;\n\n  const items: PageItem[] = [1];\n\n  if (showLeftEllipsis) {\n    items.push(ELLIPSIS);\n  } else {\n    for (let i = 2; i < leftSibling; i++) {\n      items.push(i);\n    }\n  }\n\n  for (let i = leftSibling; i <= rightSibling; i++) {\n    items.push(i);\n  }\n\n  if (showRightEllipsis) {\n    items.push(ELLIPSIS);\n  } else {\n    for (let i = rightSibling + 1; i < totalPages; i++) {\n      items.push(i);\n    }\n  }\n\n  items.push(totalPages);\n\n  return items;\n};\n\nexport default function Pagination({\n  page,\n  totalPages,\n  onPageChange,\n  siblings = 1,\n  className,\n}: PaginationProps) {\n  const shouldReduceMotion = useReducedMotion();\n  const generatedId = useId();\n  const layoutId = `pagination-active-${generatedId}`;\n\n  const pageItems = useMemo(\n    () => buildPageRange(page, totalPages, siblings),\n    [page, totalPages, siblings]\n  );\n\n  const handlePrev = useCallback(() => {\n    if (page > 1) {\n      onPageChange(page - 1);\n    }\n  }, [page, onPageChange]);\n\n  const handleNext = useCallback(() => {\n    if (page < totalPages) {\n      onPageChange(page + 1);\n    }\n  }, [page, totalPages, onPageChange]);\n\n  return (\n    <nav\n      aria-label=\"Pagination\"\n      className={cn(\"mx-auto flex w-full justify-center\", className)}\n    >\n      <ul className=\"flex flex-row items-center gap-1\">\n        {/* Previous button */}\n        <li>\n          <SmoothButton\n            aria-label=\"Go to previous page\"\n            className=\"gap-1 px-2.5\"\n            disabled={page <= 1}\n            onClick={handlePrev}\n            size=\"sm\"\n            type=\"button\"\n            variant=\"ghost\"\n          >\n            <ChevronLeft className=\"size-4\" />\n            <span className=\"hidden sm:block\">Previous</span>\n          </SmoothButton>\n        </li>\n\n        {/* Page numbers */}\n        {pageItems.map((item, index) => {\n          if (item === ELLIPSIS) {\n            return (\n              <li\n                aria-hidden=\"true\"\n                className=\"flex size-9 items-center justify-center\"\n                key={`ellipsis-${String(index)}`}\n              >\n                <motion.span\n                  animate={\n                    shouldReduceMotion\n                      ? { opacity: 1 }\n                      : { opacity: 1, transform: \"translateY(0px)\" }\n                  }\n                  className=\"text-muted-foreground text-sm\"\n                  initial={\n                    shouldReduceMotion\n                      ? { opacity: 1 }\n                      : { opacity: 0, transform: \"translateY(4px)\" }\n                  }\n                  transition={\n                    shouldReduceMotion\n                      ? { duration: 0 }\n                      : {\n                          ...SPRING_DEFAULT,\n                          delay: index * STAGGER_DELAY,\n                        }\n                  }\n                >\n                  ...\n                </motion.span>\n              </li>\n            );\n          }\n\n          const isActive = item === page;\n\n          return (\n            <motion.li\n              animate={\n                shouldReduceMotion\n                  ? { opacity: 1 }\n                  : { opacity: 1, transform: \"translateY(0px)\" }\n              }\n              initial={\n                shouldReduceMotion\n                  ? { opacity: 1 }\n                  : { opacity: 0, transform: \"translateY(4px)\" }\n              }\n              key={item}\n              transition={\n                shouldReduceMotion\n                  ? { duration: 0 }\n                  : {\n                      ...SPRING_DEFAULT,\n                      delay: index * STAGGER_DELAY,\n                    }\n              }\n            >\n              <SmoothButton\n                aria-current={isActive ? \"page\" : undefined}\n                aria-label={`Go to page ${String(item)}`}\n                className={cn(\n                  \"relative size-9\",\n                  isActive ? \"text-foreground\" : \"text-muted-foreground\"\n                )}\n                onClick={() => onPageChange(item)}\n                size=\"icon\"\n                type=\"button\"\n                variant=\"ghost\"\n              >\n                {isActive && (\n                  <motion.span\n                    className=\"absolute inset-0 rounded-md border bg-background shadow-sm\"\n                    layout\n                    layoutId={layoutId}\n                    style={{ originY: \"0px\" }}\n                    transition={\n                      shouldReduceMotion ? { duration: 0 } : SPRING_INDICATOR\n                    }\n                  />\n                )}\n                <span className=\"relative z-10\">{item}</span>\n              </SmoothButton>\n            </motion.li>\n          );\n        })}\n\n        {/* Next button */}\n        <li>\n          <SmoothButton\n            aria-label=\"Go to next page\"\n            className=\"gap-1 px-2.5\"\n            disabled={page >= totalPages}\n            onClick={handleNext}\n            size=\"sm\"\n            type=\"button\"\n            variant=\"ghost\"\n          >\n            <span className=\"hidden sm:block\">Next</span>\n            <ChevronRight className=\"size-4\" />\n          </SmoothButton>\n        </li>\n      </ul>\n    </nav>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/pagination/index.tsx","type":"registry:ui"}],"name":"pagination","registryDependencies":["https://smoothui.dev/r/smooth-button.json","https://smoothui.dev/r/lib.json"],"title":"Pagination","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"A PerCharacterRise text animation component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { motion, useInView, useReducedMotion } from \"motion/react\";\nimport { useRef } from \"react\";\n\nexport interface PerCharacterRiseProps {\n  children: string;\n  className?: string;\n  /** Delay before the animation starts, in milliseconds. */\n  delay?: number;\n  /** Per-character stagger, in milliseconds. */\n  stagger?: number;\n  /** Animate only once the text scrolls into view. */\n  triggerOnView?: boolean;\n}\n\nconst DURATION_S = 0.7;\nconst MS = 1000;\n// Apple's clean tvOS-style ease-out.\nconst EASE = [0.2, 0.8, 0.2, 1] as const;\n\n/**\n * PerCharacterRise — letters slide up from below with no blur, crisp and\n * deliberate. Apple's clean tvOS-style reveal. From the animate-text catalog\n * (`per-character-rise`). Best on 40px+ headlines.\n */\nexport default function PerCharacterRise({\n  children,\n  className = \"\",\n  delay = 0,\n  stagger = 24,\n  triggerOnView = false,\n}: PerCharacterRiseProps) {\n  const ref = useRef<HTMLSpanElement>(null);\n  const inView = useInView(ref, { once: true });\n  const shouldReduceMotion = useReducedMotion();\n  const play = (!triggerOnView || inView) && !shouldReduceMotion;\n  const characters = Array.from(children);\n\n  return (\n    <span aria-label={children} className={className} ref={ref}>\n      {characters.map((char, index) => (\n        <motion.span\n          animate={play ? { opacity: 1, y: 0 } : undefined}\n          aria-hidden=\"true\"\n          initial={shouldReduceMotion ? { opacity: 1 } : { opacity: 0, y: 32 }}\n          // biome-ignore lint/suspicious/noArrayIndexKey: characters have no stable id\n          key={index}\n          style={{ display: \"inline-block\", whiteSpace: \"pre\" }}\n          transition={\n            shouldReduceMotion\n              ? { duration: 0 }\n              : {\n                  delay: delay / MS + (index * stagger) / MS,\n                  duration: DURATION_S,\n                  ease: EASE,\n                }\n          }\n        >\n          {char === \" \" ? \" \" : String(char)}\n        </motion.span>\n      ))}\n    </span>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/per-character-rise/index.tsx","type":"registry:ui"}],"name":"per-character-rise","registryDependencies":[],"title":"Per Character Rise","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"A PerWordCrossfade text animation component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { motion, useInView, useReducedMotion } from \"motion/react\";\nimport { useRef } from \"react\";\n\nexport interface PerWordCrossfadeProps {\n  children: string;\n  className?: string;\n  /** Delay before the animation starts, in milliseconds. */\n  delay?: number;\n  /** Per-word stagger, in milliseconds. */\n  stagger?: number;\n  /** Animate only once the text scrolls into view. */\n  triggerOnView?: boolean;\n}\n\nconst DURATION_S = 0.7;\nconst MS = 1000;\n// Calm keynote ease-out.\nconst EASE = [0.16, 1, 0.3, 1] as const;\n\n/**\n * PerWordCrossfade — per-word fade-in with a subtle upward drift,\n * calm keynote rhythm. From the animate-text catalog\n * (`per-word-crossfade`).\n */\nexport default function PerWordCrossfade({\n  children,\n  className = \"\",\n  delay = 0,\n  stagger = 70,\n  triggerOnView = false,\n}: PerWordCrossfadeProps) {\n  const ref = useRef<HTMLSpanElement>(null);\n  const inView = useInView(ref, { once: true });\n  const shouldReduceMotion = useReducedMotion();\n  const play = (!triggerOnView || inView) && !shouldReduceMotion;\n  const words = children.split(\" \");\n\n  return (\n    <span aria-label={children} className={className} ref={ref}>\n      {words.map((word, index) => (\n        // biome-ignore lint/suspicious/noArrayIndexKey: words have no stable id\n        <span key={index} style={{ display: \"inline-block\" }}>\n          <motion.span\n            animate={play ? { opacity: 1, y: 0 } : undefined}\n            aria-hidden=\"true\"\n            initial={shouldReduceMotion ? { opacity: 1 } : { opacity: 0, y: 8 }}\n            style={{ display: \"inline-block\", whiteSpace: \"pre\" }}\n            transition={\n              shouldReduceMotion\n                ? { duration: 0 }\n                : {\n                    delay: delay / MS + (index * stagger) / MS,\n                    duration: DURATION_S,\n                    ease: EASE,\n                  }\n            }\n          >\n            {word}\n          </motion.span>\n          {index < words.length - 1 && (\n            <span\n              aria-hidden=\"true\"\n              style={{ display: \"inline-block\", whiteSpace: \"pre\" }}\n            >\n              {\" \"}\n            </span>\n          )}\n        </span>\n      ))}\n    </span>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/per-word-crossfade/index.tsx","type":"registry:ui"}],"name":"per-word-crossfade","registryDependencies":[],"title":"Per Word Crossfade","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"A PhotoStack component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { useRef, useState } from \"react\";\n\nexport interface PhotoStackPhoto {\n  alt: string;\n  id: string;\n  /** Optional caption shown over the front card. */\n  name?: string;\n  role?: string;\n  src: string;\n}\n\nexport interface PhotoStackProps {\n  className?: string;\n  /** Fired after the top card is sent to the back, with the new id order. */\n  onCycle?: (order: string[]) => void;\n  /** Cards to stack. The first item starts on top. */\n  photos: PhotoStackPhoto[];\n}\n\n// Resting offset/scale per stack position (0 = front).\nconst POSITIONS = [\n  { scale: 1, x: 0, y: 0 },\n  { scale: 0.95, x: 16, y: 12 },\n  { scale: 0.9, x: 32, y: 24 },\n];\n\n/**\n * PhotoStack — a draggable deck of photo cards. Drag the top card (Tinder-style)\n * in either direction, or tap it, to send it to the back and reveal the next.\n * Inspired by Josh Puckett's dialkit Photo Stack.\n */\nexport default function PhotoStack({\n  className,\n  photos,\n  onCycle,\n}: PhotoStackProps) {\n  const reduceMotion = useReducedMotion();\n  const [order, setOrder] = useState<number[]>(() => photos.map((_, i) => i));\n  // A real drag fires both onDragEnd and a trailing onClick. Without this guard\n  // a short drag would cycle twice and land the card in 2nd place, not last.\n  const justDragged = useRef(false);\n\n  const cycle = () => {\n    setOrder((prev) => {\n      const next = [...prev.slice(1), prev[0]];\n      onCycle?.(next.map((i) => photos[i].id));\n      return next;\n    });\n  };\n\n  return (\n    <div className={cn(\"relative h-72 w-56 select-none\", className)}>\n      {order.map((photoIndex, pos) => {\n        const photo = photos[photoIndex];\n        const rest = POSITIONS[Math.min(pos, POSITIONS.length - 1)];\n        const isFront = pos === 0;\n\n        return (\n          <motion.div\n            animate={{ rotate: 0, scale: rest.scale, x: rest.x, y: rest.y }}\n            aria-label={\n              isFront ? `${photo.name ?? photo.alt} — next` : undefined\n            }\n            className=\"absolute inset-0 overflow-hidden rounded-2xl border border-black/10 bg-white shadow-[0_1px_2px_rgba(0,0,0,0.06),0_14px_32px_rgba(0,0,0,0.14)] focus-visible:outline-2 focus-visible:outline-blue-600 focus-visible:outline-offset-2\"\n            drag={isFront && !reduceMotion ? \"x\" : false}\n            dragSnapToOrigin\n            key={photo.id}\n            onClick={() => {\n              if (!isFront) {\n                return;\n              }\n              // Swallow the click that trails a drag so it doesn't double-cycle.\n              if (justDragged.current) {\n                justDragged.current = false;\n                return;\n              }\n              cycle();\n            }}\n            onDragEnd={() => {\n              // Any drag release sends the dragged (front) card to the back.\n              justDragged.current = true;\n              cycle();\n            }}\n            onKeyDown={(e: React.KeyboardEvent) => {\n              if (isFront && (e.key === \"Enter\" || e.key === \" \")) {\n                e.preventDefault();\n                cycle();\n              }\n            }}\n            role={isFront ? \"button\" : undefined}\n            style={{\n              cursor: isFront ? \"grab\" : \"default\",\n              zIndex: photos.length - pos,\n            }}\n            tabIndex={isFront ? 0 : -1}\n            transition={\n              reduceMotion\n                ? { duration: 0 }\n                : { damping: 30, stiffness: 320, type: \"spring\" }\n            }\n            whileDrag={{ cursor: \"grabbing\" }}\n          >\n            <img\n              alt={photo.alt}\n              className=\"h-full w-full object-cover object-top\"\n              draggable={false}\n              src={photo.src}\n            />\n            {photo.name || photo.role ? (\n              <figcaption\n                className={cn(\n                  \"absolute inset-x-0 bottom-0 flex flex-col gap-px bg-gradient-to-t from-black/60 to-transparent p-3.5 text-white transition-opacity\",\n                  !isFront && \"opacity-0\"\n                )}\n              >\n                {photo.name ? (\n                  <span className=\"font-semibold text-sm\">{photo.name}</span>\n                ) : null}\n                {photo.role ? (\n                  <span className=\"text-xs opacity-85\">{photo.role}</span>\n                ) : null}\n              </figcaption>\n            ) : null}\n            {pos > 0 && (\n              <span\n                aria-hidden\n                className=\"pointer-events-none absolute inset-0 bg-white\"\n                style={{ opacity: pos === 1 ? 0.2 : 0.36 }}\n              />\n            )}\n          </motion.div>\n        );\n      })}\n    </div>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/photo-stack/index.tsx","type":"registry:ui"}],"name":"photo-stack","registryDependencies":[],"title":"Photo Stack","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion","@radix-ui/react-tabs"],"description":"A Phototab component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport {\n  Content as TabsContent,\n  List as TabsList,\n  Root as TabsRoot,\n  Trigger as TabsTrigger,\n} from \"@radix-ui/react-tabs\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useLayoutEffect, useRef, useState } from \"react\";\n\n/**\n * Tab definition for Phototab\n */\nexport interface PhototabTab {\n  /** Tab icon (ReactNode) */\n  icon: React.ReactNode;\n  /** Tab image (string: URL or import) */\n  image: string;\n  /** Tab label */\n  name: string;\n}\n\nexport interface PhototabProps {\n  /** Class name for root */\n  className?: string;\n  /** Default selected tab name */\n  defaultTab?: string;\n  /** Height of the component in pixels */\n  height?: number;\n  /** Class name for image */\n  imageClassName?: string;\n  /** Class name for tab list */\n  tabListClassName?: string;\n  /** Array of tabs to display */\n  tabs: PhototabTab[];\n  /** Class name for tab trigger */\n  tabTriggerClassName?: string;\n}\n\nexport default function Phototab({\n  tabs,\n  defaultTab,\n  height = 400,\n  className = \"\",\n  tabListClassName = \"\",\n  tabTriggerClassName = \"\",\n  imageClassName = \"\",\n}: PhototabProps) {\n  const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);\n  const [bgStyle, setBgStyle] = useState<{\n    left: number;\n    top: number;\n    width: number;\n    height: number;\n  } | null>(null);\n  const triggersRef = useRef<(HTMLButtonElement | null)[]>([]);\n  const listRef = useRef<HTMLDivElement | null>(null);\n  const shouldReduceMotion = useReducedMotion();\n\n  useLayoutEffect(() => {\n    if (\n      hoveredIndex !== null &&\n      triggersRef.current[hoveredIndex] &&\n      listRef.current\n    ) {\n      const trigger = triggersRef.current[hoveredIndex];\n      if (!trigger) {\n        return;\n      }\n      const listRect = listRef.current.getBoundingClientRect();\n      const triggerRect = trigger.getBoundingClientRect();\n      setBgStyle({\n        height: triggerRect.height,\n        left: triggerRect.left - listRect.left,\n        top: triggerRect.top - listRect.top,\n        width: triggerRect.width,\n      });\n    } else {\n      setBgStyle(null);\n    }\n  }, [hoveredIndex]);\n\n  return (\n    <TabsRoot\n      className={`group relative aspect-square w-auto overflow-hidden ${className}`}\n      defaultValue={defaultTab || (tabs[0]?.name ?? \"\")}\n      orientation=\"horizontal\"\n      style={{ height: `${height}px` }}\n    >\n      <TabsList\n        aria-label=\"Phototab Tabs\"\n        className={`absolute right-0 bottom-2 left-0 mx-auto flex w-40 -translate-y-10 flex-row items-center justify-between rounded-full bg-primary/40 px-3 py-2 font-medium text-sm ring ring-border/70 backdrop-blur-sm transition hover:text-foreground md:translate-y-20 md:group-hover:translate-y-0 ${tabListClassName}`}\n        ref={listRef}\n        style={{ pointerEvents: \"auto\" }}\n      >\n        <AnimatePresence>\n          {bgStyle ? (\n            <motion.span\n              animate={{\n                height: bgStyle.height,\n                left: bgStyle.left,\n                opacity: 1,\n                top: bgStyle.top,\n                width: bgStyle.width,\n              }}\n              className=\"absolute z-0 rounded-full bg-primary transition-colors\"\n              exit={{ opacity: 0 }}\n              initial={{ opacity: 0 }}\n              layoutId=\"hoverBackground\"\n              style={{ position: \"absolute\" }}\n              transition={\n                shouldReduceMotion\n                  ? { duration: 0 }\n                  : {\n                      damping: 40,\n                      duration: 0.25,\n                      stiffness: 400,\n                      type: \"spring\" as const,\n                    }\n              }\n            />\n          ) : null}\n        </AnimatePresence>\n        {tabs.map((tab, index) => (\n          <TabsTrigger\n            aria-label={tab.name}\n            className={`relative z-10 cursor-pointer rounded-full p-2 data-[state='active']:bg-background ${tabTriggerClassName}`}\n            key={tab.name}\n            onMouseEnter={() => {\n              setHoveredIndex(index);\n            }}\n            onMouseLeave={() => {\n              setHoveredIndex(null);\n            }}\n            ref={(el) => {\n              triggersRef.current[index] = el;\n            }}\n            value={tab.name}\n          >\n            <span className=\"relative z-10 rounded-full focus:outline-none\">\n              {tab.icon}\n              <span className=\"sr-only\">{tab.name}</span>\n            </span>\n          </TabsTrigger>\n        ))}\n      </TabsList>\n      {tabs.map((tab) => (\n        <TabsContent className=\"h-full w-full\" key={tab.name} value={tab.name}>\n          <img\n            alt={tab.name}\n            className={`h-full w-full rounded-2xl bg-primary object-cover ${imageClassName}`}\n            draggable={false}\n            height={height}\n            loading=\"lazy\"\n            src={tab.image}\n            width={400}\n          />\n        </TabsContent>\n      ))}\n    </TabsRoot>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/phototab/index.tsx","type":"registry:ui"}],"name":"phototab","registryDependencies":[],"title":"Phototab","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion","lucide-react"],"description":"A PowerOffSlide component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { Power } from \"lucide-react\";\nimport {\n  motion,\n  useAnimation,\n  useAnimationFrame,\n  useMotionValue,\n  useReducedMotion,\n} from \"motion/react\";\nimport { type RefObject, useRef, useState } from \"react\";\n\nconst SLIDE_THRESHOLD = 160;\nconst SLIDE_MAX_DISTANCE = 168;\nconst PERCENTAGE_MULTIPLIER = 100;\n\nexport interface PowerOffSlideProps {\n  className?: string;\n  disabled?: boolean;\n  duration?: number;\n  label?: string;\n  onPowerOff?: () => void;\n}\n\nexport default function PowerOffSlide({\n  onPowerOff,\n  label = \"Slide to power off\",\n  className = \"\",\n  duration = 2000,\n  disabled = false,\n}: PowerOffSlideProps) {\n  const [isPoweringOff, setIsPoweringOff] = useState(false);\n  const x = useMotionValue(0);\n  const controls = useAnimation();\n  const constraintsRef = useRef(null);\n  const textRef: RefObject<HTMLDivElement | null> = useRef(null);\n  const shouldReduceMotion = useReducedMotion();\n\n  useAnimationFrame((t) => {\n    if (shouldReduceMotion) {\n      return;\n    }\n    const animDuration = duration;\n    const progress = (t % animDuration) / animDuration;\n    if (textRef.current) {\n      textRef.current.style.setProperty(\n        \"--x\",\n        `${(1 - progress) * PERCENTAGE_MULTIPLIER}%`\n      );\n    }\n  });\n\n  const handleDragEnd = async () => {\n    if (disabled) {\n      return;\n    }\n    const dragDistance = x.get();\n    if (dragDistance > SLIDE_THRESHOLD) {\n      await controls.start({ x: SLIDE_MAX_DISTANCE });\n      setIsPoweringOff(true);\n      if (onPowerOff) {\n        onPowerOff();\n      }\n      setTimeout(() => {\n        setIsPoweringOff(false);\n        controls.start({ x: 0 });\n        x.set(0);\n      }, duration);\n    } else {\n      controls.start({ x: 0 });\n    }\n  };\n\n  return (\n    <div className={`flex h-auto items-center justify-center ${className}`}>\n      <div className=\"w-56\">\n        {isPoweringOff ? (\n          <div className=\"text-center text-foreground\">\n            <p className=\"mb-2 font-light text-xl\">Shutting down...</p>\n          </div>\n        ) : (\n          <div\n            className=\"relative h-14 overflow-hidden rounded-full border bg-secondary\"\n            ref={constraintsRef}\n          >\n            <div className=\"absolute inset-0 left-8 z-0 flex items-center justify-center overflow-hidden\">\n              <div className=\"loading-shimmer relative w-full select-none text-center font-normal text-foreground text-md\">\n                {label}\n              </div>\n            </div>\n            <motion.div\n              animate={controls}\n              aria-disabled={disabled}\n              className={`absolute top-1 left-1 z-10 flex h-12 w-12 items-center justify-center rounded-full bg-background shadow-md ${disabled ? \"cursor-not-allowed opacity-50\" : \"cursor-grab active:cursor-grabbing\"}`}\n              drag={disabled || shouldReduceMotion ? false : \"x\"}\n              dragConstraints={{ left: 0, right: SLIDE_MAX_DISTANCE }}\n              dragElastic={0}\n              dragMomentum={false}\n              onDragEnd={handleDragEnd}\n              style={{ x }}\n              tabIndex={disabled ? -1 : 0}\n              transition={\n                shouldReduceMotion\n                  ? { duration: 0 }\n                  : { duration: 0.25, type: \"spring\" as const }\n              }\n            >\n              <Power className=\"text-red-600\" size={32} />\n            </motion.div>\n          </div>\n        )}\n      </div>\n    </div>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/power-off-slide/index.tsx","type":"registry:ui"}],"name":"power-off-slide","registryDependencies":[],"title":"Power Off Slide","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":[],"description":"A PriceFlow component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { useEffect, useRef, useState } from \"react\";\n\nconst STAGGER_DELAY = 50;\n\nexport interface PriceFlowProps {\n  className?: string;\n  value: number;\n}\n\nconst animateDigit = (\n  prevElement: HTMLElement | null,\n  nextElement: HTMLElement | null,\n  isIncreasing: boolean\n) => {\n  if (prevElement === null || nextElement === null) {\n    return;\n  }\n\n  if (isIncreasing) {\n    prevElement.classList.add(\"slide-out-up\");\n    nextElement.classList.add(\"slide-in-up\");\n  } else {\n    prevElement.classList.add(\"slide-out-down\");\n    nextElement.classList.add(\"slide-in-down\");\n  }\n\n  const handleAnimationEnd = () => {\n    prevElement.classList.remove(\"slide-out-up\", \"slide-out-down\");\n    nextElement.classList.remove(\"slide-in-up\", \"slide-in-down\");\n    prevElement.removeEventListener(\"animationend\", handleAnimationEnd);\n  };\n\n  prevElement.addEventListener(\"animationend\", handleAnimationEnd);\n};\n\nexport default function PriceFlow({ value, className = \"\" }: PriceFlowProps) {\n  const [prevValue, setPrevValue] = useState(value);\n\n  // Create refs for each digit position (tens and ones)\n  const prevTensRef = useRef<HTMLElement>(null);\n  const nextTensRef = useRef<HTMLElement>(null);\n  const prevOnesRef = useRef<HTMLElement>(null);\n  const nextOnesRef = useRef<HTMLElement>(null);\n\n  useEffect(() => {\n    if (value === prevValue) {\n      return;\n    }\n\n    const prevTens = prevTensRef.current;\n    const nextTens = nextTensRef.current;\n    const prevOnes = prevOnesRef.current;\n    const nextOnes = nextOnesRef.current;\n\n    const prevTensValue = Math.floor(prevValue / 10);\n    const currentTensValue = Math.floor(value / 10);\n    const tensChanged = currentTensValue !== prevTensValue;\n\n    if (tensChanged && prevTens && nextTens) {\n      const isTensIncreasing = currentTensValue > prevTensValue;\n      animateDigit(prevTens, nextTens, isTensIncreasing);\n    }\n\n    const prevOnesValue = prevValue % 10;\n    const currentOnesValue = value % 10;\n    const onesChanged = currentOnesValue !== prevOnesValue;\n\n    if (onesChanged && prevOnes && nextOnes) {\n      const isOnesIncreasing = currentOnesValue > prevOnesValue;\n      setTimeout(() => {\n        animateDigit(prevOnes, nextOnes, isOnesIncreasing);\n      }, STAGGER_DELAY);\n    }\n\n    setPrevValue(value);\n  }, [value, prevValue]);\n\n  const formatValue = (val: number) => val.toString().padStart(2, \"0\");\n\n  const prevFormatted = formatValue(prevValue);\n  const currentFormatted = formatValue(value);\n\n  return (\n    <span className={cn(\"relative inline-flex items-center\", className)}>\n      <span className=\"relative inline-block overflow-hidden\">\n        {/* Tens digit */}\n        <span\n          className=\"absolute inset-0 flex items-center justify-center\"\n          ref={prevTensRef}\n          style={{ transform: \"translateY(-100%)\" }}\n        >\n          {prevFormatted[0]}\n        </span>\n        <span\n          className=\"flex items-center justify-center\"\n          ref={nextTensRef}\n          style={{ transform: \"translateY(0%)\" }}\n        >\n          {currentFormatted[0]}\n        </span>\n      </span>\n\n      <span className=\"relative inline-block overflow-hidden\">\n        {/* Ones digit */}\n        <span\n          className=\"absolute inset-0 flex items-center justify-center\"\n          ref={prevOnesRef}\n          style={{ transform: \"translateY(-100%)\" }}\n        >\n          {prevFormatted[1]}\n        </span>\n        <span\n          className=\"flex items-center justify-center\"\n          ref={nextOnesRef}\n          style={{ transform: \"translateY(0%)\" }}\n        >\n          {currentFormatted[1]}\n        </span>\n      </span>\n    </span>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/price-flow/index.tsx","type":"registry:ui"}],"name":"price-flow","registryDependencies":[],"title":"Price Flow","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"A persistent WebGL prism sweep transition wrapper for route and state changes.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { useReducedMotion } from \"motion/react\";\nimport {\n  type ReactNode,\n  type RefObject,\n  useEffect,\n  useRef,\n  useState,\n} from \"react\";\n\nexport type PrismSweepDirection = \"left\" | \"right\" | \"up\" | \"down\";\nexport type PrismSweepTone = \"prism\" | \"chroma\" | \"aperture\" | \"beam\";\n\nexport interface PrismSweepTransitionProps {\n  /** Content rendered underneath the sweep. */\n  children: ReactNode;\n  /** Optional wrapper className. */\n  className?: string;\n  /** Direction the prism field travels from. */\n  direction?: PrismSweepDirection;\n  /** Total sweep duration, in milliseconds. */\n  duration?: number;\n  /** Called after the sweep finishes. */\n  onRest?: () => void;\n  /** Optional tone preset for the shader palette and field behavior. */\n  tone?: PrismSweepTone;\n  /** Key that starts a new sweep when it changes. */\n  transitionKey: string | number;\n}\n\nconst DEFAULT_DURATION_MS = 1240;\nconst OUTRO_MS = 82;\nconst CONTENT_SWAP_RATIO = 0.52;\nconst MAX_DEVICE_PIXEL_RATIO = 2;\nconst MIN_PHASE_MS = 80;\nconst HIDDEN_OPACITY = 0;\nconst VISIBLE_OPACITY = 1;\n\nconst VERTEX_SHADER = `\nattribute vec2 position;\nvarying vec2 vUv;\n\nvoid main() {\n  vUv = position * 0.5 + 0.5;\n  gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst FRAGMENT_SHADER = `\nprecision mediump float;\n\nvarying vec2 vUv;\n\nuniform vec2 uResolution;\nuniform float uClock;\nuniform float uTravel;\nuniform float uOpacity;\nuniform float uAxis;\nuniform float uForward;\nuniform float uSpan;\nuniform float uSoftness;\nuniform float uBend;\nuniform float uFacet;\nuniform float uBloom;\nuniform vec3 uInk;\nuniform vec3 uPearl;\nuniform vec3 uWarm;\nuniform vec3 uCool;\n\nfloat saturate(float value) {\n  return clamp(value, 0.0, 1.0);\n}\n\nfloat triangleWave(float value) {\n  return abs(fract(value) * 2.0 - 1.0);\n}\n\nfloat softRidge(float distance, float width) {\n  return exp(-distance * distance / max(width, 0.0001));\n}\n\nvoid main() {\n  vec2 uv = vUv;\n  vec2 pixel = 1.0 / max(uResolution, vec2(1.0));\n\n  float mainAxis = mix(uv.x, uv.y, uAxis);\n  float crossAxis = mix(uv.y, uv.x, uAxis);\n  mainAxis = mix(mainAxis, 1.0 - mainAxis, step(0.0, -uForward));\n\n  float time = uClock * 0.42;\n  float drift =\n      sin(crossAxis * 7.0 + time * 1.8) * 0.024\n    + sin(crossAxis * 17.0 - time * 1.1 + 1.6) * 0.010\n    + sin((mainAxis + crossAxis) * 11.0 + time * 0.7) * 0.006;\n  drift *= uBend;\n\n  float travel = -0.34 + uTravel * 1.68;\n  float d = mainAxis - travel - drift;\n  float ad = abs(d);\n\n  float field = 1.0 - smoothstep(uSpan, uSpan + uSoftness, ad);\n  float inner = 1.0 - smoothstep(uSpan * 0.28, uSpan, ad);\n  float frontRidge = softRidge(d - uSpan * 0.34, 0.0017 + pixel.x * 16.0);\n  float backRidge = softRidge(d + uSpan * 0.54, 0.0035 + pixel.y * 14.0) * 0.55;\n\n  float facetGrid = triangleWave(crossAxis * uFacet + mainAxis * 2.1 + time * 0.35);\n  float facetCuts = smoothstep(0.18, 0.92, facetGrid);\n  float diagonal = triangleWave((crossAxis - mainAxis * 0.36) * (uFacet * 0.44) - time * 0.2);\n  float caustic = pow(1.0 - diagonal, 4.0) * 0.42 + pow(facetCuts, 2.2) * 0.22;\n\n  float glass = saturate(field * (0.5 + inner * 0.42) + frontRidge * 0.52 + backRidge * 0.2);\n  float gateIn = smoothstep(0.02, 0.14, uTravel);\n  float gateOut = 1.0 - smoothstep(0.9, 1.0, uTravel);\n  float envelope = gateIn * gateOut;\n\n  vec3 base = mix(uCool, uWarm, saturate(crossAxis * 0.82 + drift * 4.5));\n  vec3 prism = mix(base, uPearl, inner * 0.36 + frontRidge * 0.5);\n  prism = mix(prism, uInk, smoothstep(0.55, 0.0, ad) * 0.1);\n  prism += caustic * uBloom * mix(uWarm, uPearl, 0.38);\n  prism += frontRidge * uBloom * mix(uWarm, uCool, 0.22) * 0.72;\n  prism += backRidge * uBloom * uCool * 0.7;\n\n  float vignette = smoothstep(0.0, 0.025, crossAxis) * smoothstep(1.0, 0.975, crossAxis);\n  float alpha = saturate(glass * 0.64 + frontRidge * 0.24 + backRidge * 0.14);\n  alpha *= uOpacity * envelope * vignette;\n\n  gl_FragColor = vec4(prism * alpha, alpha);\n}\n`;\n\nconst FULLSCREEN_TRIANGLES = new Float32Array([\n  -1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1,\n]);\n\nconst TONE_CONFIGS = {\n  aperture: {\n    bend: 0.42,\n    bloom: 0.86,\n    cool: [0.92, 0.78, 0.66],\n    facet: 9.5,\n    ink: [0.24, 0.16, 0.12],\n    pearl: [1, 0.96, 0.88],\n    softness: 0.076,\n    span: 0.17,\n    warm: [1, 0.62, 0.38],\n  },\n  beam: {\n    bend: 0.12,\n    bloom: 1.08,\n    cool: [0.86, 0.72, 0.5],\n    facet: 18,\n    ink: [0.2, 0.13, 0.07],\n    pearl: [1, 0.9, 0.68],\n    softness: 0.038,\n    span: 0.092,\n    warm: [1, 0.48, 0.18],\n  },\n  chroma: {\n    bend: 0.9,\n    bloom: 0.78,\n    cool: [0.38, 0.86, 0.84],\n    facet: 13.5,\n    ink: [0.18, 0.16, 0.3],\n    pearl: [0.96, 0.88, 1],\n    softness: 0.082,\n    span: 0.15,\n    warm: [1, 0.34, 0.72],\n  },\n  prism: {\n    bend: 0.76,\n    bloom: 0.58,\n    cool: [0.28, 0.78, 0.86],\n    facet: 12,\n    ink: [0.2, 0.16, 0.26],\n    pearl: [0.94, 0.82, 0.9],\n    softness: 0.07,\n    span: 0.14,\n    warm: [0.98, 0.28, 0.58],\n  },\n} as const satisfies Record<\n  PrismSweepTone,\n  {\n    bend: number;\n    bloom: number;\n    cool: readonly [number, number, number];\n    facet: number;\n    ink: readonly [number, number, number];\n    pearl: readonly [number, number, number];\n    softness: number;\n    span: number;\n    warm: readonly [number, number, number];\n  }\n>;\n\ninterface UniformMap {\n  uAxis: WebGLUniformLocation | null;\n  uBend: WebGLUniformLocation | null;\n  uBloom: WebGLUniformLocation | null;\n  uClock: WebGLUniformLocation | null;\n  uCool: WebGLUniformLocation | null;\n  uFacet: WebGLUniformLocation | null;\n  uForward: WebGLUniformLocation | null;\n  uInk: WebGLUniformLocation | null;\n  uOpacity: WebGLUniformLocation | null;\n  uPearl: WebGLUniformLocation | null;\n  uResolution: WebGLUniformLocation | null;\n  uSoftness: WebGLUniformLocation | null;\n  uSpan: WebGLUniformLocation | null;\n  uTravel: WebGLUniformLocation | null;\n  uWarm: WebGLUniformLocation | null;\n}\n\ninterface ShaderController {\n  destroy: () => void;\n  getTravel: () => number;\n  setDirection: (direction: PrismSweepDirection) => void;\n  setOpacity: (opacity: number) => void;\n  setTone: (tone: PrismSweepTone) => void;\n  setTravel: (travel: number) => void;\n}\n\ninterface ShaderControllerState {\n  animationFrame: number;\n  buffer: WebGLBuffer | null;\n  direction: PrismSweepDirection;\n  gl: WebGLRenderingContext;\n  opacity: number;\n  program: WebGLProgram;\n  startedAt: number;\n  tone: PrismSweepTone;\n  travel: number;\n  uniforms: UniformMap;\n}\n\ninterface SweepPlayback {\n  cancel: () => void;\n}\n\nconst clamp01 = (value: number) => Math.max(0, Math.min(1, value));\n\nconst directionToShader = (direction: PrismSweepDirection) => {\n  switch (direction) {\n    case \"right\":\n      return { axis: 0, forward: -1 };\n    case \"up\":\n      return { axis: 1, forward: 1 };\n    case \"down\":\n      return { axis: 1, forward: -1 };\n    default:\n      return { axis: 0, forward: 1 };\n  }\n};\n\nconst easePrismTravel = (progress: number) => {\n  const clamped = clamp01(progress);\n  return 1 - (1 - clamped) ** 3.35;\n};\n\nconst easeOpacityOut = (progress: number) => {\n  const clamped = clamp01(progress);\n  return clamped * clamped * (3 - 2 * clamped);\n};\n\nconst compileShader = (\n  gl: WebGLRenderingContext,\n  type: number,\n  source: string\n) => {\n  const shader = gl.createShader(type);\n  if (!shader) {\n    return null;\n  }\n\n  gl.shaderSource(shader, source);\n  gl.compileShader(shader);\n\n  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n    gl.deleteShader(shader);\n    return null;\n  }\n\n  return shader;\n};\n\nconst createProgram = (gl: WebGLRenderingContext) => {\n  const vertex = compileShader(gl, gl.VERTEX_SHADER, VERTEX_SHADER);\n  const fragment = compileShader(gl, gl.FRAGMENT_SHADER, FRAGMENT_SHADER);\n\n  if (!(vertex && fragment)) {\n    return null;\n  }\n\n  const program = gl.createProgram();\n  if (!program) {\n    return null;\n  }\n\n  gl.attachShader(program, vertex);\n  gl.attachShader(program, fragment);\n  gl.linkProgram(program);\n  gl.deleteShader(vertex);\n  gl.deleteShader(fragment);\n\n  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n    gl.deleteProgram(program);\n    return null;\n  }\n\n  return program;\n};\n\nconst resizeCanvas = (canvas: HTMLCanvasElement, gl: WebGLRenderingContext) => {\n  const rect = canvas.getBoundingClientRect();\n  const dpr = Math.min(window.devicePixelRatio || 1, MAX_DEVICE_PIXEL_RATIO);\n  const width = Math.max(1, Math.round(rect.width * dpr));\n  const height = Math.max(1, Math.round(rect.height * dpr));\n\n  if (canvas.width !== width || canvas.height !== height) {\n    canvas.width = width;\n    canvas.height = height;\n  }\n\n  gl.viewport(0, 0, width, height);\n};\n\nconst getUniforms = (gl: WebGLRenderingContext, program: WebGLProgram) => ({\n  uAxis: gl.getUniformLocation(program, \"uAxis\"),\n  uBend: gl.getUniformLocation(program, \"uBend\"),\n  uBloom: gl.getUniformLocation(program, \"uBloom\"),\n  uClock: gl.getUniformLocation(program, \"uClock\"),\n  uCool: gl.getUniformLocation(program, \"uCool\"),\n  uFacet: gl.getUniformLocation(program, \"uFacet\"),\n  uForward: gl.getUniformLocation(program, \"uForward\"),\n  uInk: gl.getUniformLocation(program, \"uInk\"),\n  uOpacity: gl.getUniformLocation(program, \"uOpacity\"),\n  uPearl: gl.getUniformLocation(program, \"uPearl\"),\n  uResolution: gl.getUniformLocation(program, \"uResolution\"),\n  uSoftness: gl.getUniformLocation(program, \"uSoftness\"),\n  uSpan: gl.getUniformLocation(program, \"uSpan\"),\n  uTravel: gl.getUniformLocation(program, \"uTravel\"),\n  uWarm: gl.getUniformLocation(program, \"uWarm\"),\n});\n\nconst setRgbUniform = (\n  gl: WebGLRenderingContext,\n  location: WebGLUniformLocation | null,\n  value: readonly [number, number, number]\n) => {\n  gl.uniform3f(location, value[0], value[1], value[2]);\n};\n\nconst drawShader = (\n  canvas: HTMLCanvasElement,\n  state: ShaderControllerState\n) => {\n  const { gl, uniforms } = state;\n  const directionUniforms = directionToShader(state.direction);\n  const toneConfig = TONE_CONFIGS[state.tone];\n\n  resizeCanvas(canvas, gl);\n  gl.clearColor(0, 0, 0, 0);\n  gl.clear(gl.COLOR_BUFFER_BIT);\n\n  gl.uniform2f(uniforms.uResolution, canvas.width, canvas.height);\n  gl.uniform1f(uniforms.uClock, (performance.now() - state.startedAt) / 1000);\n  gl.uniform1f(uniforms.uTravel, state.travel);\n  gl.uniform1f(uniforms.uOpacity, state.opacity);\n  gl.uniform1f(uniforms.uAxis, directionUniforms.axis);\n  gl.uniform1f(uniforms.uForward, directionUniforms.forward);\n  gl.uniform1f(uniforms.uSpan, toneConfig.span);\n  gl.uniform1f(uniforms.uSoftness, toneConfig.softness);\n  gl.uniform1f(uniforms.uBend, toneConfig.bend);\n  gl.uniform1f(uniforms.uFacet, toneConfig.facet);\n  gl.uniform1f(uniforms.uBloom, toneConfig.bloom);\n  setRgbUniform(gl, uniforms.uInk, toneConfig.ink);\n  setRgbUniform(gl, uniforms.uPearl, toneConfig.pearl);\n  setRgbUniform(gl, uniforms.uWarm, toneConfig.warm);\n  setRgbUniform(gl, uniforms.uCool, toneConfig.cool);\n  gl.drawArrays(gl.TRIANGLES, 0, 6);\n};\n\nconst createShaderController = (\n  canvas: HTMLCanvasElement,\n  direction: PrismSweepDirection,\n  tone: PrismSweepTone\n) => {\n  const gl = canvas.getContext(\"webgl\", {\n    alpha: true,\n    antialias: false,\n    depth: false,\n    premultipliedAlpha: true,\n    preserveDrawingBuffer: false,\n    stencil: false,\n  });\n\n  if (!gl) {\n    return null;\n  }\n\n  const program = createProgram(gl);\n  if (!program) {\n    return null;\n  }\n\n  const buffer = gl.createBuffer();\n  const position = gl.getAttribLocation(program, \"position\");\n\n  // biome-ignore lint/correctness/useHookAtTopLevel: WebGLRenderingContext.useProgram is not a React hook.\n  gl.useProgram(program);\n  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);\n  gl.bufferData(gl.ARRAY_BUFFER, FULLSCREEN_TRIANGLES, gl.STATIC_DRAW);\n  gl.enableVertexAttribArray(position);\n  gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0);\n  gl.enable(gl.BLEND);\n  gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);\n\n  const state: ShaderControllerState = {\n    animationFrame: 0,\n    buffer,\n    direction,\n    gl,\n    opacity: HIDDEN_OPACITY,\n    program,\n    startedAt: performance.now(),\n    tone,\n    travel: 0,\n    uniforms: getUniforms(gl, program),\n  };\n\n  let destroyed = false;\n  const tick = () => {\n    if (destroyed) {\n      return;\n    }\n\n    drawShader(canvas, state);\n    state.animationFrame = requestAnimationFrame(tick);\n  };\n\n  state.animationFrame = requestAnimationFrame(tick);\n\n  return {\n    destroy: () => {\n      destroyed = true;\n      cancelAnimationFrame(state.animationFrame);\n      state.gl.clear(state.gl.COLOR_BUFFER_BIT);\n      if (state.buffer) {\n        state.gl.deleteBuffer(state.buffer);\n      }\n      state.gl.deleteProgram(state.program);\n    },\n    getTravel: () => state.travel,\n    setDirection: (nextDirection: PrismSweepDirection) => {\n      state.direction = nextDirection;\n    },\n    setOpacity: (opacity: number) => {\n      state.opacity = clamp01(opacity);\n    },\n    setTone: (nextTone: PrismSweepTone) => {\n      state.tone = nextTone;\n    },\n    setTravel: (travel: number) => {\n      state.travel = clamp01(travel);\n    },\n  } satisfies ShaderController;\n};\n\nconst useShaderController = ({\n  canvasRef,\n  direction,\n  tone,\n}: {\n  canvasRef: RefObject<HTMLCanvasElement | null>;\n  direction: PrismSweepDirection;\n  tone: PrismSweepTone;\n}) => {\n  const controllerRef = useRef<ShaderController | null>(null);\n\n  useEffect(() => {\n    const canvas = canvasRef.current;\n    if (!canvas) {\n      return;\n    }\n\n    const controller = createShaderController(canvas, direction, tone);\n    controllerRef.current = controller;\n\n    return () => {\n      controller?.destroy();\n      controllerRef.current = null;\n    };\n  }, [canvasRef, direction, tone]);\n\n  useEffect(() => {\n    controllerRef.current?.setDirection(direction);\n    controllerRef.current?.setTone(tone);\n  }, [direction, tone]);\n\n  return controllerRef;\n};\n\nconst playFrameSweep = (\n  controller: ShaderController,\n  {\n    direction,\n    duration,\n    midpoint,\n    onComplete,\n    onMidpoint,\n    outroMs,\n  }: {\n    direction: PrismSweepDirection;\n    duration: number;\n    midpoint: number;\n    onComplete: () => void;\n    onMidpoint: () => void;\n    outroMs: number;\n  }\n): SweepPlayback => {\n  let animationFrame = 0;\n  let midpointReached = false;\n  let cancelled = false;\n  const startedAt = performance.now();\n  const startTravel = controller.getTravel();\n  const sweepMs = Math.max(MIN_PHASE_MS, duration * (1 - startTravel));\n\n  controller.setDirection(direction);\n  controller.setOpacity(VISIBLE_OPACITY);\n\n  const finishSweep = () => {\n    const outroStartedAt = performance.now();\n\n    const fadeOut = () => {\n      if (cancelled) {\n        return;\n      }\n\n      const outroRaw = clamp01((performance.now() - outroStartedAt) / outroMs);\n      controller.setOpacity(VISIBLE_OPACITY * (1 - easeOpacityOut(outroRaw)));\n\n      if (outroRaw < 1) {\n        animationFrame = requestAnimationFrame(fadeOut);\n        return;\n      }\n\n      controller.setOpacity(HIDDEN_OPACITY);\n      controller.setTravel(0);\n      onComplete();\n    };\n\n    animationFrame = requestAnimationFrame(fadeOut);\n  };\n\n  const advance = () => {\n    if (cancelled) {\n      return;\n    }\n\n    const raw = clamp01((performance.now() - startedAt) / sweepMs);\n    const nextTravel = startTravel + (1 - startTravel) * easePrismTravel(raw);\n    controller.setTravel(nextTravel);\n\n    if (!(midpointReached || nextTravel < midpoint)) {\n      midpointReached = true;\n      onMidpoint();\n    }\n\n    if (raw < 1) {\n      animationFrame = requestAnimationFrame(advance);\n      return;\n    }\n\n    if (!midpointReached) {\n      onMidpoint();\n    }\n    controller.setTravel(1);\n    finishSweep();\n  };\n\n  animationFrame = requestAnimationFrame(advance);\n\n  return {\n    cancel: () => {\n      cancelled = true;\n      cancelAnimationFrame(animationFrame);\n    },\n  };\n};\n\n/**\n * PrismSweepTransition — a frame-level route/state transition with a custom\n * faceted caustic shader tuned for SmoothUI previews.\n */\nconst PrismSweepTransition = ({\n  children,\n  className,\n  direction = \"left\",\n  duration = DEFAULT_DURATION_MS,\n  onRest,\n  tone = \"prism\",\n  transitionKey,\n}: PrismSweepTransitionProps) => {\n  const shouldReduceMotion = useReducedMotion();\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const playbackRef = useRef<SweepPlayback | null>(null);\n  const [renderedChildren, setRenderedChildren] = useState(children);\n  const [isSweeping, setIsSweeping] = useState(false);\n  const previousKey = useRef(transitionKey);\n  const controllerRef = useShaderController({ canvasRef, direction, tone });\n\n  useEffect(() => {\n    if (previousKey.current === transitionKey) {\n      setRenderedChildren(children);\n      return;\n    }\n\n    previousKey.current = transitionKey;\n    playbackRef.current?.cancel();\n\n    if (shouldReduceMotion) {\n      setRenderedChildren(children);\n      setIsSweeping(false);\n      onRest?.();\n      return;\n    }\n\n    const controller = controllerRef.current;\n    if (!controller) {\n      setRenderedChildren(children);\n      setIsSweeping(false);\n      onRest?.();\n      return;\n    }\n\n    setIsSweeping(true);\n    playbackRef.current = playFrameSweep(controller, {\n      direction,\n      duration: Math.max(MIN_PHASE_MS, duration),\n      midpoint: CONTENT_SWAP_RATIO,\n      onComplete: () => {\n        setIsSweeping(false);\n        playbackRef.current = null;\n        onRest?.();\n      },\n      onMidpoint: () => setRenderedChildren(children),\n      outroMs: OUTRO_MS,\n    });\n\n    return () => {\n      playbackRef.current?.cancel();\n    };\n  }, [\n    children,\n    controllerRef,\n    direction,\n    duration,\n    onRest,\n    shouldReduceMotion,\n    transitionKey,\n  ]);\n\n  return (\n    <div\n      className={cn(\n        \"relative isolate overflow-hidden rounded-2xl bg-background text-foreground\",\n        className\n      )}\n      data-sweeping={isSweeping ? \"true\" : \"false\"}\n    >\n      <div className=\"relative z-0\">{renderedChildren}</div>\n      <canvas\n        className={cn(\n          \"pointer-events-none absolute inset-0 z-10 h-full w-full transition-opacity duration-75\",\n          isSweeping && !shouldReduceMotion ? \"opacity-100\" : \"opacity-0\"\n        )}\n        ref={canvasRef}\n      />\n    </div>\n  );\n};\n\nexport default PrismSweepTransition;\n","path":"index.tsx","target":"components/smoothui/prism-sweep-transition/index.tsx","type":"registry:ui"}],"name":"prism-sweep-transition","registryDependencies":[],"title":"Prism Sweep Transition","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"Animated product card component for ecommerce","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport SmoothButton from \"@/components/smoothui/smooth-button\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useEffect, useId, useState } from \"react\";\n\nexport interface ProductCardProps {\n  badge?: string;\n  className?: string;\n  currency?: string;\n  image: string;\n  onAddToCart?: () => void;\n  onWishlist?: () => void;\n  originalPrice?: number;\n  price: number;\n  rating?: number;\n  title: string;\n}\n\n/* ─────────────────────────────────────────────────────────\n * ANIMATION STORYBOARD\n *\n *    0ms   card enters viewport → fade up + scale\n *  250ms   badge pops in with spring\n *  hover   image zooms 1.05, wishlist heart appears\n *  click   button scale bounce + icon morph to checkmark\n * ───────────────────────────────────────────────────────── */\n\nconst SPRING = {\n  bounce: 0.1,\n  duration: 0.25,\n  type: \"spring\" as const,\n};\n\nconst SPRING_BOUNCY = {\n  bounce: 0.2,\n  duration: 0.3,\n  type: \"spring\" as const,\n};\n\nconst STAR_PATH =\n  \"M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.562.562 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.562.562 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z\";\n\nfunction StarIcon({ filled, half }: { filled: boolean; half?: boolean }) {\n  const id = useId();\n\n  if (half) {\n    const gradientId = `half-star-${id}`;\n    return (\n      <svg\n        aria-hidden=\"true\"\n        className=\"h-3.5 w-3.5 text-amber-400\"\n        viewBox=\"0 0 24 24\"\n      >\n        <defs>\n          <linearGradient id={gradientId}>\n            <stop offset=\"50%\" stopColor=\"currentColor\" />\n            <stop offset=\"50%\" stopColor=\"transparent\" />\n          </linearGradient>\n        </defs>\n        <path\n          d={STAR_PATH}\n          fill={`url(#${gradientId})`}\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          strokeWidth={1.5}\n        />\n      </svg>\n    );\n  }\n\n  return (\n    <svg\n      aria-hidden=\"true\"\n      className={cn(\n        \"h-3.5 w-3.5\",\n        filled ? \"text-amber-400\" : \"text-muted-foreground/30\"\n      )}\n      fill={filled ? \"currentColor\" : \"none\"}\n      stroke=\"currentColor\"\n      strokeWidth={filled ? 0 : 1.5}\n      viewBox=\"0 0 24 24\"\n    >\n      <path d={STAR_PATH} strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n    </svg>\n  );\n}\n\nfunction CheckIcon() {\n  return (\n    <svg\n      aria-hidden=\"true\"\n      className=\"h-4 w-4\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth={2.5}\n      viewBox=\"0 0 24 24\"\n    >\n      <path d=\"M5 13l4 4L19 7\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n    </svg>\n  );\n}\n\nfunction CartIcon() {\n  return (\n    <svg\n      aria-hidden=\"true\"\n      className=\"h-4 w-4\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth={2}\n      viewBox=\"0 0 24 24\"\n    >\n      <path\n        d=\"M15.75 10.5V6a3.75 3.75 0 10-7.5 0v4.5m11.356-1.993l1.263 12c.07.665-.45 1.243-1.119 1.243H4.25a1.125 1.125 0 01-1.12-1.243l1.264-12A1.125 1.125 0 015.513 7.5h12.974c.576 0 1.059.435 1.119 1.007zM8.625 10.5a.375.375 0 11-.75 0 .375.375 0 01.75 0zm7.5 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n    </svg>\n  );\n}\n\nfunction HeartIcon({ filled }: { filled: boolean }) {\n  return (\n    <svg\n      aria-hidden=\"true\"\n      className={cn(\"h-4 w-4\", filled ? \"text-red-500\" : \"text-foreground\")}\n      fill={filled ? \"currentColor\" : \"none\"}\n      stroke=\"currentColor\"\n      strokeWidth={filled ? 0 : 2}\n      viewBox=\"0 0 24 24\"\n    >\n      <path\n        d=\"M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n    </svg>\n  );\n}\n\nfunction RatingStars({ rating, title }: { rating: number; title: string }) {\n  const fullStars = Math.floor(rating);\n  const hasHalf = rating - fullStars >= 0.25 && rating - fullStars < 0.75;\n  const roundedUp = rating - fullStars >= 0.75;\n\n  return (\n    <div\n      aria-label={`${rating} out of 5 stars`}\n      className=\"flex items-center gap-0.5\"\n    >\n      {Array.from({ length: 5 }, (_, i) => {\n        const isFilled = i < fullStars || (roundedUp && i === fullStars);\n        const isHalf = hasHalf && i === fullStars;\n        return (\n          <StarIcon\n            filled={isFilled}\n            half={isHalf}\n            key={`star-${title}-${i}`}\n          />\n        );\n      })}\n      <span className=\"ml-1 font-medium text-muted-foreground text-xs\">\n        {rating}\n      </span>\n    </div>\n  );\n}\n\nexport default function ProductCard({\n  image,\n  title,\n  price,\n  originalPrice,\n  currency = \"$\",\n  rating,\n  badge,\n  onAddToCart,\n  onWishlist,\n  className,\n}: ProductCardProps) {\n  const shouldReduceMotion = useReducedMotion();\n  const [isHoverDevice, setIsHoverDevice] = useState(false);\n  const [isAdded, setIsAdded] = useState(false);\n  const [isWishlisted, setIsWishlisted] = useState(false);\n\n  useEffect(() => {\n    const mediaQuery = window.matchMedia(\"(hover: hover) and (pointer: fine)\");\n    setIsHoverDevice(mediaQuery.matches);\n    const handler = (e: MediaQueryListEvent) => setIsHoverDevice(e.matches);\n    mediaQuery.addEventListener(\"change\", handler);\n    return () => mediaQuery.removeEventListener(\"change\", handler);\n  }, []);\n\n  const handleAddToCart = () => {\n    setIsAdded(true);\n    onAddToCart?.();\n    setTimeout(() => setIsAdded(false), 2000);\n  };\n\n  const handleWishlist = () => {\n    setIsWishlisted((prev) => !prev);\n    onWishlist?.();\n  };\n\n  const hasDiscount = originalPrice !== undefined && originalPrice > price;\n  const discountPercent = hasDiscount\n    ? Math.round(((originalPrice - price) / originalPrice) * 100)\n    : 0;\n\n  return (\n    <motion.div\n      aria-label={`${title} - ${currency}${price}`}\n      className={cn(\n        \"group relative flex w-full flex-col overflow-hidden rounded-2xl border bg-card shadow-sm\",\n        \"transition-shadow duration-300\",\n        isHoverDevice && \"hover:shadow-black/5 hover:shadow-xl\",\n        className\n      )}\n      initial={\n        shouldReduceMotion\n          ? { opacity: 1 }\n          : { opacity: 0, transform: \"translateY(20px) scale(0.97)\" }\n      }\n      role=\"article\"\n      transition={shouldReduceMotion ? { duration: 0 } : SPRING}\n      viewport={{ margin: \"-50px\", once: true }}\n      whileInView={\n        shouldReduceMotion\n          ? { opacity: 1 }\n          : { opacity: 1, transform: \"translateY(0px) scale(1)\" }\n      }\n    >\n      {/* Image — full bleed */}\n      <div className=\"relative aspect-square overflow-hidden bg-muted\">\n        <img\n          alt={title}\n          className={cn(\n            \"h-full w-full object-cover\",\n            !shouldReduceMotion &&\n              \"transition-transform duration-500 ease-[cubic-bezier(0.23,1,0.32,1)]\",\n            isHoverDevice && !shouldReduceMotion && \"group-hover:scale-105\"\n          )}\n          draggable={false}\n          src={image}\n        />\n\n        {/* Hover overlay gradient */}\n        <div\n          className={cn(\n            \"pointer-events-none absolute inset-0 bg-gradient-to-t from-black/10 via-transparent to-transparent opacity-0 transition-opacity duration-300\",\n            isHoverDevice && \"group-hover:opacity-100\"\n          )}\n        />\n\n        {/* Badge */}\n        {badge ? (\n          <motion.span\n            animate={\n              shouldReduceMotion\n                ? { opacity: 1 }\n                : { opacity: 1, transform: \"scale(1)\" }\n            }\n            className={cn(\n              \"absolute top-3 left-3 rounded-full px-2.5 py-1 font-semibold text-xs shadow-sm\",\n              badge.toLowerCase() === \"sale\"\n                ? \"bg-red-500 text-white\"\n                : badge.toLowerCase() === \"new\"\n                  ? \"bg-emerald-500 text-white\"\n                  : \"bg-primary text-primary-foreground\"\n            )}\n            initial={\n              shouldReduceMotion\n                ? { opacity: 1 }\n                : { opacity: 0, transform: \"scale(0.6)\" }\n            }\n            role=\"status\"\n            transition={shouldReduceMotion ? { duration: 0 } : SPRING_BOUNCY}\n          >\n            {badge}\n          </motion.span>\n        ) : null}\n\n        {/* Wishlist button */}\n        <motion.button\n          aria-label={\n            isWishlisted\n              ? `Remove ${title} from wishlist`\n              : `Add ${title} to wishlist`\n          }\n          className={cn(\n            \"absolute top-3 right-3 flex h-8 w-8 cursor-pointer items-center justify-center rounded-full bg-background/80 backdrop-blur-sm\",\n            \"transition-colors duration-150\",\n            \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n            isHoverDevice ? \"opacity-0 group-hover:opacity-100\" : \"opacity-100\"\n          )}\n          onClick={handleWishlist}\n          type=\"button\"\n          whileTap={\n            shouldReduceMotion\n              ? undefined\n              : { scale: 0.85, transition: { duration: 0.1 } }\n          }\n        >\n          <HeartIcon filled={isWishlisted} />\n        </motion.button>\n      </div>\n\n      {/* Content */}\n      <div className=\"flex flex-1 flex-col gap-2 p-4\">\n        <h3 className=\"line-clamp-1 font-semibold text-foreground text-sm tracking-tight\">\n          {title}\n        </h3>\n\n        {rating !== undefined && <RatingStars rating={rating} title={title} />}\n\n        <div className=\"flex items-baseline gap-2\">\n          <span className=\"font-bold text-foreground text-xl tracking-tight\">\n            {currency}\n            {price}\n          </span>\n          {hasDiscount ? (\n            <>\n              <span className=\"text-muted-foreground text-sm line-through\">\n                {currency}\n                {originalPrice}\n              </span>\n              <span className=\"rounded-md bg-red-50 px-1.5 py-0.5 font-semibold text-red-600 text-xs dark:bg-red-950/40 dark:text-red-400\">\n                -{discountPercent}%\n              </span>\n            </>\n          ) : null}\n        </div>\n\n        <div className=\"mt-auto pt-2\">\n          <SmoothButton\n            aria-label={`Add ${title} to cart`}\n            className={cn(\n              \"w-full gap-2\",\n              isAdded && \"bg-emerald-600 text-white hover:bg-emerald-600\"\n            )}\n            disabled={isAdded}\n            onClick={handleAddToCart}\n            size=\"default\"\n            variant=\"candy\"\n          >\n            <AnimatePresence initial={false} mode=\"wait\">\n              {isAdded ? (\n                <motion.span\n                  animate={{ opacity: 1, transform: \"scale(1)\" }}\n                  className=\"flex items-center gap-2\"\n                  exit={{ opacity: 0, transform: \"scale(0.8)\" }}\n                  initial={{ opacity: 0, transform: \"scale(0.8)\" }}\n                  key=\"added\"\n                  transition={\n                    shouldReduceMotion ? { duration: 0 } : { duration: 0.15 }\n                  }\n                >\n                  <CheckIcon /> Added\n                </motion.span>\n              ) : (\n                <motion.span\n                  animate={{ opacity: 1, transform: \"scale(1)\" }}\n                  className=\"flex items-center gap-2\"\n                  exit={{ opacity: 0, transform: \"scale(0.8)\" }}\n                  initial={{ opacity: 0, transform: \"scale(0.8)\" }}\n                  key=\"cart\"\n                  transition={\n                    shouldReduceMotion ? { duration: 0 } : { duration: 0.15 }\n                  }\n                >\n                  <CartIcon /> Add to Cart\n                </motion.span>\n              )}\n            </AnimatePresence>\n          </SmoothButton>\n        </div>\n      </div>\n    </motion.div>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/product-card/index.tsx","type":"registry:ui"}],"name":"product-card","registryDependencies":["https://smoothui.dev/r/smooth-button.json"],"title":"Product Card","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":[],"description":"A radial SDF circle pattern transition inspired by the Codrops radial circles step.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport SdfBlobTransition from \"../sdf-blob-transition\";\n\nexport interface RadialCirclesTransitionProps {\n  children: ReactNode;\n  className?: string;\n  duration?: number;\n  onRest?: () => void;\n  transitionKey: string | number;\n}\n\nconst RadialCirclesTransition = ({\n  children,\n  className,\n  duration = 1120,\n  onRest,\n  transitionKey,\n}: RadialCirclesTransitionProps) => (\n  <SdfBlobTransition\n    className={className}\n    duration={duration}\n    onRest={onRest}\n    transitionKey={transitionKey}\n    variant=\"radial\"\n  >\n    {children}\n  </SdfBlobTransition>\n);\n\nexport default RadialCirclesTransition;\n","path":"index.tsx","target":"components/smoothui/radial-circles-transition/index.tsx","type":"registry:ui"}],"name":"radial-circles-transition","registryDependencies":["https://smoothui.dev/r/sdf-blob-transition.json"],"title":"Radial Circles Transition","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion","radix-ui"],"description":"An animated Radio Group component for SmoothUI with selection indicator spring animation.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { RadioGroup as RadioGroupPrimitive } from \"radix-ui\";\nimport type React from \"react\";\nimport { Children, cloneElement, isValidElement } from \"react\";\nimport { SPRING_DEFAULT } from \"@/components/smoothui/lib/animation\";\n\nexport interface RadioGroupProps {\n  /** Radio items to render */\n  children: React.ReactNode;\n  /** Optional CSS class for the group container */\n  className?: string;\n  /** The default value when uncontrolled */\n  defaultValue?: string;\n  /** Whether the entire group is disabled */\n  disabled?: boolean;\n  /** Name attribute for form submission */\n  name?: string;\n  /** Callback when the selected value changes */\n  onValueChange?: (value: string) => void;\n  /** Orientation of the radio group for arrow key navigation */\n  orientation?: \"horizontal\" | \"vertical\";\n  /** Whether a selection is required */\n  required?: boolean;\n  /** The controlled value of the selected radio item */\n  value?: string;\n}\n\nexport interface RadioProps {\n  /** @internal Stagger index passed by RadioGroup */\n  _staggerIndex?: number;\n  /** Children (label content) to render next to the radio */\n  children?: React.ReactNode;\n  /** Optional CSS class */\n  className?: string;\n  /** Whether this radio is disabled */\n  disabled?: boolean;\n  /** ID for label association */\n  id?: string;\n  /** The value of this radio option */\n  value: string;\n}\n\n/** Spring for the selection dot — playful bounce per CLAUDE.md (0.2-0.3 for playful interactions) */\nconst SPRING_DOT = {\n  bounce: 0.2,\n  duration: 0.25,\n  type: \"spring\" as const,\n};\n\n/** Stagger delay per radio item on initial render */\nconst STAGGER_DELAY = 0.04;\n\nexport function RadioGroup({\n  value,\n  defaultValue,\n  onValueChange,\n  disabled = false,\n  className,\n  children,\n  name,\n  required,\n  orientation = \"vertical\",\n}: RadioGroupProps) {\n  const shouldReduceMotion = useReducedMotion();\n\n  // Inject stagger index into Radio children for entrance stagger\n  let radioIndex = 0;\n  const enhancedChildren = Children.map(children, (child) => {\n    if (\n      isValidElement(child) &&\n      (child.type === Radio ||\n        (child.type as { displayName?: string })?.displayName === \"Radio\")\n    ) {\n      const currentIndex = radioIndex;\n      radioIndex += 1;\n      return cloneElement(child as React.ReactElement<RadioProps>, {\n        _staggerIndex: currentIndex,\n      });\n    }\n    return child;\n  });\n\n  return (\n    <RadioGroupPrimitive.Root\n      className={cn(\n        orientation === \"vertical\" ? \"grid gap-3\" : \"flex items-center gap-4\",\n        className\n      )}\n      data-slot=\"radio-group\"\n      defaultValue={defaultValue}\n      disabled={disabled}\n      name={name}\n      onValueChange={onValueChange}\n      orientation={orientation}\n      required={required}\n      value={value}\n    >\n      {/* biome-ignore lint/suspicious/noLeakedRender: both branches are ReactNode, which is exactly what a wrapper should render */}\n      {shouldReduceMotion ? children : enhancedChildren}\n    </RadioGroupPrimitive.Root>\n  );\n}\n\nexport function Radio({\n  value,\n  disabled = false,\n  className,\n  id,\n  children,\n  _staggerIndex,\n}: RadioProps) {\n  const shouldReduceMotion = useReducedMotion();\n  const staggerDelay =\n    _staggerIndex === undefined ? 0 : _staggerIndex * STAGGER_DELAY;\n\n  const wrapper = (content: React.ReactNode) => {\n    if (shouldReduceMotion || _staggerIndex === undefined) {\n      return <div className=\"flex items-center gap-2\">{content}</div>;\n    }\n    return (\n      <motion.div\n        animate={{ opacity: 1, transform: \"translateY(0px)\" }}\n        className=\"flex items-center gap-2\"\n        initial={{ opacity: 0, transform: \"translateY(4px)\" }}\n        transition={{\n          ...SPRING_DEFAULT,\n          delay: staggerDelay,\n        }}\n      >\n        {content}\n      </motion.div>\n    );\n  };\n\n  return wrapper(\n    <>\n      <motion.div\n        className=\"relative\"\n        transition={shouldReduceMotion ? { duration: 0 } : SPRING_DEFAULT}\n        whileHover={shouldReduceMotion ? {} : { transform: \"scale(1.1)\" }}\n      >\n        <RadioGroupPrimitive.Item\n          className={cn(\n            \"group/radio aspect-square size-4 shrink-0 rounded-full border border-input bg-background shadow-xs outline-none transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=checked]:border-brand dark:bg-input/30 dark:aria-invalid:ring-destructive/40\",\n            className\n          )}\n          data-slot=\"radio-group-item\"\n          disabled={disabled}\n          id={id}\n          value={value}\n        >\n          <RadioGroupPrimitive.Indicator\n            className=\"flex items-center justify-center\"\n            data-slot=\"radio-group-indicator\"\n          >\n            <motion.span\n              animate={\n                shouldReduceMotion\n                  ? { opacity: 1 }\n                  : { opacity: 1, transform: \"scale(1)\" }\n              }\n              className=\"block size-2 rounded-full bg-brand\"\n              exit={\n                shouldReduceMotion\n                  ? { opacity: 0, transition: { duration: 0 } }\n                  : {\n                      opacity: 0,\n                      transform: \"scale(0)\",\n                      transition: { ...SPRING_DEFAULT, bounce: 0 },\n                    }\n              }\n              initial={\n                shouldReduceMotion\n                  ? { opacity: 1 }\n                  : { opacity: 0, transform: \"scale(0)\" }\n              }\n              transition={shouldReduceMotion ? { duration: 0 } : SPRING_DOT}\n            />\n          </RadioGroupPrimitive.Indicator>\n        </RadioGroupPrimitive.Item>\n      </motion.div>\n      {children ? (\n        <label\n          className={cn(\n            \"font-medium text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\",\n            disabled && \"cursor-not-allowed opacity-70\"\n          )}\n          htmlFor={id}\n        >\n          {children}\n        </label>\n      ) : null}\n    </>\n  );\n}\n\nRadio.displayName = \"Radio\";\n\nexport default RadioGroup;\n","path":"index.tsx","target":"components/smoothui/radio-group/index.tsx","type":"registry:ui"}],"name":"radio-group","registryDependencies":["https://smoothui.dev/r/lib.json","https://smoothui.dev/r/tokens.json"],"title":"Radio Group","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"A RevealText component for SmoothUI.","devDependencies":[],"files":[{"content":"import { motion, useInView, useReducedMotion } from \"motion/react\";\nimport React from \"react\";\n\nexport interface RevealTextProps {\n  children: string;\n  className?: string;\n  delay?: number;\n  direction?: \"up\" | \"down\" | \"left\" | \"right\";\n  triggerOnView?: boolean;\n}\n\nconst REVEAL_ANIMATION_DURATION_S = 0.25;\nconst MILLISECONDS_TO_SECONDS = 1000;\n\nconst directionVariants = {\n  down: { opacity: 0, y: -24 },\n  left: { opacity: 0, x: 24 },\n  right: { opacity: 0, x: -24 },\n  up: { opacity: 0, y: 24 },\n};\n\nconst RevealText: React.FC<RevealTextProps> = ({\n  children,\n  direction = \"up\",\n  delay = 0,\n  triggerOnView = false,\n  className = \"\",\n}) => {\n  const ref = React.useRef<HTMLSpanElement>(null);\n  const inView = useInView(ref, { once: true });\n  const shouldReduceMotion = useReducedMotion();\n  const animate = (!triggerOnView || inView) && !shouldReduceMotion;\n\n  return (\n    <motion.span\n      animate={\n        shouldReduceMotion || !animate\n          ? { opacity: 1 }\n          : { opacity: 1, x: 0, y: 0 }\n      }\n      className={className}\n      initial={\n        shouldReduceMotion ? { opacity: 1 } : directionVariants[direction]\n      }\n      ref={ref}\n      style={{ display: \"inline-block\" }}\n      transition={\n        shouldReduceMotion\n          ? { duration: 0 }\n          : {\n              delay: delay / MILLISECONDS_TO_SECONDS,\n              duration: REVEAL_ANIMATION_DURATION_S,\n            }\n      }\n    >\n      {children}\n    </motion.span>\n  );\n};\n\nexport default RevealText;\n","path":"index.tsx","target":"components/smoothui/reveal-text/index.tsx","type":"registry:ui"}],"name":"reveal-text","registryDependencies":[],"title":"Reveal Text","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion","lucide-react"],"description":"A ReviewsCarousel component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { ChevronLeft, ChevronRight } from \"lucide-react\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { useEffect, useMemo, useState } from \"react\";\n\nconst FRAME_OFFSET = -30;\nconst FRAMES_VISIBLE_LENGTH = 3;\n\nfunction clamp(val: number, [min, max]: [number, number]): number {\n  return Math.min(Math.max(val, min), max);\n}\n\nexport interface Review {\n  author: string;\n  body: string;\n  id: string | number;\n  title: string;\n}\n\ninterface ReviewCardProps {\n  activeIndex: number;\n  index: number;\n  review: Review;\n  totalCards: number;\n}\n\nfunction ReviewCard({\n  review,\n  index,\n  activeIndex,\n  totalCards,\n}: ReviewCardProps) {\n  const shouldReduceMotion = useReducedMotion();\n  const offsetIndex = index - activeIndex;\n\n  // Same logic as time-machine\n  const blur = activeIndex > index ? 2 : 0;\n  const opacity = activeIndex > index ? 0 : 1;\n  const scale = shouldReduceMotion\n    ? 1\n    : clamp(1 - offsetIndex * 0.08, [0.08, 2]);\n  const y = shouldReduceMotion\n    ? 0\n    : clamp(offsetIndex * FRAME_OFFSET, [\n        FRAME_OFFSET * FRAMES_VISIBLE_LENGTH,\n        Number.POSITIVE_INFINITY,\n      ]);\n\n  const isActive = index === activeIndex;\n\n  return (\n    <motion.figure\n      animate={{\n        scale,\n        transition: {\n          damping: 20,\n          duration: 0.25,\n          mass: 0.5,\n          stiffness: 250,\n          type: \"spring\" as const,\n        },\n        y,\n      }}\n      className={cn(\n        \"absolute left-1/2 w-[calc(100%-2rem)] max-w-[600px] -translate-x-1/2 -translate-y-1/2 rounded-2xl border border-foreground/10 bg-background/80 p-4 shadow-lg backdrop-blur-md sm:p-6\"\n      )}\n      initial={false}\n      style={{\n        borderWidth: 1 / scale,\n        filter: `blur(${blur}px)`,\n        opacity,\n        pointerEvents: isActive ? \"auto\" : \"none\",\n        top: \"50%\", // Centrar verticalmente\n        transitionDuration: shouldReduceMotion ? \"0ms\" : \"250ms\",\n        transitionProperty: \"opacity, filter\",\n        transitionTimingFunction: \"cubic-bezier(0.4, 0, 0.2, 1)\",\n        willChange: \"opacity, filter, transform\",\n        zIndex: totalCards - index,\n      }}\n    >\n      <blockquote className=\"relative\">\n        <div className=\"absolute -top-1 -left-2 text-4xl text-foreground/10 leading-none dark:text-foreground/5\">\n          \"\n        </div>\n        <p className=\"relative text-foreground/80 text-sm leading-relaxed\">\n          {review.body}\n        </p>\n      </blockquote>\n      <figcaption className=\"mt-4 flex items-center gap-2 border-foreground/5 border-t pt-4\">\n        <div className=\"flex flex-col\">\n          <span className=\"font-semibold text-foreground text-xs\">\n            {review.author}\n          </span>\n          <span className=\"text-foreground/50 text-xs\">{review.title}</span>\n        </div>\n      </figcaption>\n    </motion.figure>\n  );\n}\n\ninterface NavigationButtonProps {\n  direction: \"prev\" | \"next\";\n  disabled: boolean;\n  onClick: () => void;\n}\n\nfunction NavigationButton({\n  direction,\n  onClick,\n  disabled,\n}: NavigationButtonProps) {\n  const Icon = direction === \"prev\" ? ChevronLeft : ChevronRight;\n\n  return (\n    <button\n      aria-label={direction === \"prev\" ? \"Anterior\" : \"Siguiente\"}\n      className={cn(\n        \"box-gen group relative z-0 flex h-7 w-7 items-center justify-center rounded-full border-[0.5px] border-foreground/10 bg-background/50 backdrop-blur-sm transition-all duration-200\",\n        disabled\n          ? \"cursor-not-allowed opacity-30\"\n          : \"cursor-pointer hover:border-foreground/20 hover:bg-background/70 hover:shadow-lg\",\n        \"dark:border-foreground/5 dark:bg-foreground/5 dark:hover:border-foreground/10 dark:hover:bg-foreground/10\"\n      )}\n      disabled={disabled}\n      onClick={onClick}\n      type=\"button\"\n    >\n      <Icon\n        className={cn(\n          \"h-3.5 w-3.5 text-foreground/60 transition-colors\",\n          \"group-hover:text-foreground group-disabled:text-foreground/20\"\n        )}\n      />\n    </button>\n  );\n}\n\nexport interface ReviewsCarouselProps {\n  autoPlay?: boolean;\n  autoPlayInterval?: number;\n  className?: string;\n  excludeIds?: (string | number)[];\n  height?: string;\n  reviews: Review[];\n  showIndicators?: boolean;\n  showNavigation?: boolean;\n}\n\nexport default function ReviewsCarousel({\n  reviews,\n  className = \"\",\n  height = \"300px\",\n  excludeIds = [],\n  showIndicators = true,\n  showNavigation = true,\n  autoPlay = false,\n  autoPlayInterval = 5000,\n}: ReviewsCarouselProps) {\n  // Filter out excluded reviews - use Set for O(1) lookups\n  const filteredReviews = useMemo(() => {\n    if (excludeIds.length === 0) {\n      return reviews;\n    }\n\n    const excludeSet = new Set(excludeIds);\n    const reviewsLength = reviews.length;\n    const results: typeof reviews = [];\n\n    // Use for loop for better performance\n    for (let i = 0; i < reviewsLength; i++) {\n      const review = reviews[i];\n      if (!excludeSet.has(review.id)) {\n        results.push(review);\n      }\n    }\n\n    return results;\n  }, [reviews, excludeIds]);\n\n  const maxIndex = filteredReviews.length - 1;\n  const [activeIndex, setActiveIndex] = useState(0);\n\n  // Auto-play functionality\n  useEffect(() => {\n    if (!autoPlay || maxIndex < 0) {\n      return;\n    }\n\n    const interval = setInterval(() => {\n      setActiveIndex((prevIndex) => {\n        if (prevIndex >= maxIndex) {\n          return 0;\n        }\n        return prevIndex + 1;\n      });\n    }, autoPlayInterval);\n\n    return () => {\n      clearInterval(interval);\n    };\n  }, [autoPlay, autoPlayInterval, maxIndex]);\n\n  // Keyboard navigation\n  useEffect(() => {\n    function handleKeyDown(event: KeyboardEvent) {\n      if (event.key === \"ArrowLeft\") {\n        setActiveIndex((i) => clamp(i - 1, [0, maxIndex]));\n      } else if (event.key === \"ArrowRight\") {\n        setActiveIndex((i) => clamp(i + 1, [0, maxIndex]));\n      }\n    }\n\n    window.addEventListener(\"keydown\", handleKeyDown);\n    return () => {\n      window.removeEventListener(\"keydown\", handleKeyDown);\n    };\n  }, [maxIndex]);\n\n  const goToPrevious = () => {\n    setActiveIndex((prevIndex) => {\n      if (prevIndex > 0) {\n        return prevIndex - 1;\n      }\n      return prevIndex;\n    });\n  };\n\n  const goToNext = () => {\n    setActiveIndex((prevIndex) => {\n      const newIndex = prevIndex + 1;\n      return newIndex <= maxIndex ? newIndex : prevIndex;\n    });\n  };\n\n  if (filteredReviews.length === 0) {\n    return null;\n  }\n\n  return (\n    <div\n      className={cn(\"relative mx-auto w-full max-w-4xl\", className)}\n      style={{ height }}\n    >\n      {/* Stack of cards - using grid-stack pattern */}\n      <div className=\"relative h-full w-full py-8\">\n        <div className=\"grid h-full w-full place-items-center\">\n          {filteredReviews.map((review: Review, index: number) => (\n            <ReviewCard\n              activeIndex={activeIndex}\n              index={index}\n              key={review.id}\n              review={review}\n              totalCards={filteredReviews.length}\n            />\n          ))}\n        </div>\n      </div>\n\n      {/* Navigation buttons */}\n      {showNavigation || showIndicators ? (\n        <div className=\"absolute bottom-4 left-1/2 z-50 flex -translate-x-1/2 items-center gap-2\">\n          {showNavigation ? (\n            <NavigationButton\n              direction=\"prev\"\n              disabled={activeIndex <= 0}\n              onClick={goToPrevious}\n            />\n          ) : null}\n          {showIndicators ? (\n            <div className=\"flex items-center gap-2\">\n              {filteredReviews.map((review: Review, index: number) => (\n                <button\n                  aria-label={`Ir al testimonio ${index + 1}`}\n                  className={cn(\n                    \"h-2 rounded-full transition-all duration-200\",\n                    index === activeIndex\n                      ? \"w-8 bg-brand\"\n                      : \"w-2 bg-brand/30 hover:bg-brand/50\"\n                  )}\n                  key={review.id}\n                  onClick={() => {\n                    setActiveIndex(index);\n                  }}\n                  type=\"button\"\n                />\n              ))}\n            </div>\n          ) : null}\n          {showNavigation ? (\n            <NavigationButton\n              direction=\"next\"\n              disabled={activeIndex === maxIndex}\n              onClick={goToNext}\n            />\n          ) : null}\n        </div>\n      ) : null}\n    </div>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/reviews-carousel/index.tsx","type":"registry:ui"}],"name":"reviews-carousel","registryDependencies":["https://smoothui.dev/r/tokens.json"],"title":"Reviews Carousel","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion","lucide-react","@radix-ui/react-popover"],"description":"A RichPopover component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport {\n  Arrow as PopoverArrow,\n  Content as PopoverContent,\n  Portal as PopoverPortal,\n  Root as PopoverRoot,\n  Trigger as PopoverTrigger,\n} from \"@radix-ui/react-popover\";\nimport { Clock, ExternalLink, Play } from \"lucide-react\";\nimport { motion, useReducedMotion } from \"motion/react\";\n\nimport type * as React from \"react\";\n\nexport interface RichTooltipProps {\n  actionHref?: string;\n  actionLabel?: string;\n  align?: \"start\" | \"center\" | \"end\";\n  className?: string;\n  description?: string;\n  href?: string;\n  icon?: React.ReactNode;\n  meta?: string;\n  onActionClick?: () => void;\n  side?: \"top\" | \"bottom\" | \"left\" | \"right\";\n  title: string;\n  trigger: React.ReactNode;\n}\n\nexport function YouTubeIcon({\n  className = \"h-4 w-4 fill-red-600\",\n}: {\n  className?: string;\n}) {\n  return (\n    <svg\n      aria-label=\"YouTube\"\n      className={className}\n      focusable=\"false\"\n      height=\"16\"\n      role=\"img\"\n      viewBox=\"0 0 16 16\"\n      width=\"16\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <path d=\"M8.051 1.999h.089c.822.003 4.987.033 6.11.335a2.01 2.01 0 0 1 1.415 1.42c.101.38.172.883.22 1.402l.01.104.022.26.008.104c.065.914.073 1.77.074 1.957v.075c-.001.194-.01 1.108-.082 2.06l-.008.105-.009.104c-.05.572-.124 1.14-.235 1.558a2.01 2.01 0 0 1-1.415 1.42c-1.16.312-5.569.334-6.18.335h-.142c-.309 0-1.587-.006-2.927-.052l-.17-.006-.087-.004-.171-.007-.171-.007c-1.11-.049-2.167-.128-2.654-.26a2.01 2.01 0 0 1-1.415-1.419c-.111-.417-.185-.986-.235-1.558L.09 9.82l-.008-.104A31 31 0 0 1 0 7.68v-.123c.002-.215.01-.958.064-1.778l.007-.103.003-.052.008-.104.022-.26.01-.104c.048-.519.119-1.023.22-1.402a2.01 2.01 0 0 1 1.415-1.42c.487-.13 1.544-.21 2.654-.26l.17-.007.172-.006.086-.003.171-.007A100 100 0 0 1 7.858 2zM6.4 5.209v4.818l4.157-2.408z\" />\n    </svg>\n  );\n}\n\nexport default function RichTooltip({\n  trigger,\n  title,\n  description,\n  icon,\n  href,\n  actionLabel,\n  actionHref,\n  onActionClick,\n  meta,\n  className = \"\",\n  side = \"top\",\n  align = \"center\",\n}: RichTooltipProps) {\n  const shouldReduceMotion = useReducedMotion();\n  const Title = (\n    <div className=\"flex items-center gap-2 font-medium text-sm\">\n      {icon ?? <YouTubeIcon />}\n      {href ? (\n        <a\n          className=\"inline-flex items-center gap-1 hover:underline\"\n          href={href}\n          rel=\"noopener noreferrer\"\n          target=\"_blank\"\n        >\n          <span>{title}</span>\n          <ExternalLink className=\"h-3.5 w-3.5 opacity-70\" />\n        </a>\n      ) : (\n        <span>{title}</span>\n      )}\n    </div>\n  );\n\n  const renderAction = () => {\n    if (!actionLabel) {\n      return null;\n    }\n\n    const actionClassName =\n      \"inline-flex items-center gap-2 rounded-full bg-white px-3 py-2 font-medium text-black text-xs transition-colors hover:bg-white/90\";\n\n    if (actionHref) {\n      return (\n        <a\n          className={actionClassName}\n          href={actionHref}\n          rel=\"noopener noreferrer\"\n          target=\"_blank\"\n        >\n          <Play className=\"h-3.5 w-3.5\" /> {actionLabel}\n        </a>\n      );\n    }\n\n    return (\n      <button className={actionClassName} onClick={onActionClick} type=\"button\">\n        <Play className=\"h-3.5 w-3.5\" /> {actionLabel}\n      </button>\n    );\n  };\n\n  const Action = renderAction();\n\n  return (\n    <PopoverRoot>\n      <PopoverTrigger asChild>{trigger}</PopoverTrigger>\n      <PopoverPortal>\n        <PopoverContent\n          align={align}\n          asChild\n          className={`z-50 ${className}`}\n          side={side}\n          sideOffset={8}\n        >\n          <motion.div\n            animate={\n              shouldReduceMotion\n                ? { opacity: 1 }\n                : { filter: \"blur(0px)\", opacity: 1, scale: 1, y: 0 }\n            }\n            className=\"relative rounded-2xl border border-white/10 bg-black px-4 py-3 text-white shadow-xl\"\n            exit={\n              shouldReduceMotion\n                ? { opacity: 0, transition: { duration: 0 } }\n                : { filter: \"blur(8px)\", opacity: 0, scale: 0.95, y: 5 }\n            }\n            initial={\n              shouldReduceMotion\n                ? { opacity: 1 }\n                : { filter: \"blur(8px)\", opacity: 0, scale: 0.95, y: 5 }\n            }\n            transition={\n              shouldReduceMotion\n                ? { duration: 0 }\n                : {\n                    damping: 30,\n                    duration: 0.2,\n                    stiffness: 500,\n                    type: \"spring\" as const,\n                  }\n            }\n          >\n            {Title}\n            {description ? (\n              <p className=\"mt-3 max-w-xs text-balance text-base text-white/90 leading-relaxed\">\n                {description}\n              </p>\n            ) : null}\n            {meta || Action ? (\n              <div className=\"mt-4 flex items-center justify-between gap-3\">\n                {meta ? (\n                  <span className=\"inline-flex items-center gap-1 rounded-full bg-white/10 px-3 py-1 text-white text-xs\">\n                    <Clock className=\"h-3.5 w-3.5\" /> {meta}\n                  </span>\n                ) : (\n                  <span />\n                )}\n                {Action}\n              </div>\n            ) : null}\n\n            {/* Tail */}\n            <PopoverArrow className=\"fill-black\" />\n          </motion.div>\n        </PopoverContent>\n      </PopoverPortal>\n    </PopoverRoot>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/rich-popover/index.tsx","type":"registry:ui"}],"name":"rich-popover","registryDependencies":[],"title":"Rich Popover","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"A ScaleDownFade text animation component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { motion, useInView, useReducedMotion } from \"motion/react\";\nimport { useRef } from \"react\";\n\nexport interface ScaleDownFadeProps {\n  children: string;\n  className?: string;\n  /** Delay before the animation starts, in milliseconds. */\n  delay?: number;\n  /** Animate only once the text scrolls into view. */\n  triggerOnView?: boolean;\n}\n\nconst DURATION_S = 0.52;\nconst MS = 1000;\nconst EASE = [0.22, 1, 0.36, 1] as const;\n\n/**\n * ScaleDownFade — subtle premium settle-in where the text enters slightly\n * scaled up and drifts down to its natural size. The whole text animates as\n * a single element. From the animate-text catalog (`scale-down-fade`).\n * Safe default for product UIs where copy should feel polished but not animated.\n */\nexport default function ScaleDownFade({\n  children,\n  className = \"\",\n  delay = 0,\n  triggerOnView = false,\n}: ScaleDownFadeProps) {\n  const ref = useRef<HTMLSpanElement>(null);\n  const inView = useInView(ref, { once: true });\n  const shouldReduceMotion = useReducedMotion();\n  const play = (!triggerOnView || inView) && !shouldReduceMotion;\n\n  return (\n    <motion.span\n      animate={play ? { opacity: 1, scale: 1, y: 0 } : undefined}\n      aria-label={children}\n      className={className}\n      initial={\n        shouldReduceMotion ? { opacity: 1 } : { opacity: 0, scale: 1.04, y: 8 }\n      }\n      ref={ref}\n      style={{ display: \"inline-block\" }}\n      transition={\n        shouldReduceMotion\n          ? { duration: 0 }\n          : {\n              delay: delay / MS,\n              duration: DURATION_S,\n              ease: EASE,\n            }\n      }\n    >\n      {children}\n    </motion.span>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/scale-down-fade/index.tsx","type":"registry:ui"}],"name":"scale-down-fade","registryDependencies":[],"title":"Scale Down Fade","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":[],"description":"A ScrambleHover component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport type React from \"react\";\nimport { useEffect, useRef, useState } from \"react\";\n\nexport interface ScrambleHoverProps {\n  children: string;\n  className?: string;\n  duration?: number; // total animation duration in ms\n  speed?: number; // interval between scrambles in ms\n}\n\nconst CHARACTERS =\n  \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+-=<>?\".split(\n    \"\"\n  );\n\nfunction scrambleText(original: string) {\n  return original\n    .split(\"\")\n    .map((char) =>\n      char === \" \"\n        ? \" \"\n        : CHARACTERS[Math.floor(Math.random() * CHARACTERS.length)]\n    )\n    .join(\"\");\n}\n\nconst ScrambleHover: React.FC<ScrambleHoverProps> = ({\n  children,\n  duration = 600,\n  speed = 30,\n  className = \"\",\n}) => {\n  const [display, setDisplay] = useState(children);\n  const [shouldReduceMotion, setShouldReduceMotion] = useState(false);\n  const [isHoverDevice, setIsHoverDevice] = useState(false);\n  const timeoutRef = useRef<NodeJS.Timeout | null>(null);\n  const intervalRef = useRef<NodeJS.Timeout | null>(null);\n\n  useEffect(() => {\n    const motionQuery = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    const hoverQuery = window.matchMedia(\"(hover: hover) and (pointer: fine)\");\n\n    setShouldReduceMotion(motionQuery.matches);\n    setIsHoverDevice(hoverQuery.matches);\n\n    const handleMotionChange = (e: MediaQueryListEvent) => {\n      setShouldReduceMotion(e.matches);\n    };\n    const handleHoverChange = (e: MediaQueryListEvent) => {\n      setIsHoverDevice(e.matches);\n    };\n\n    motionQuery.addEventListener(\"change\", handleMotionChange);\n    hoverQuery.addEventListener(\"change\", handleHoverChange);\n\n    return () => {\n      motionQuery.removeEventListener(\"change\", handleMotionChange);\n      hoverQuery.removeEventListener(\"change\", handleHoverChange);\n    };\n  }, []);\n\n  const handleMouseEnter = () => {\n    if (shouldReduceMotion || !isHoverDevice) {\n      return;\n    }\n    if (intervalRef.current) {\n      clearInterval(intervalRef.current);\n    }\n    if (timeoutRef.current) {\n      clearTimeout(timeoutRef.current);\n    }\n    intervalRef.current = setInterval(() => {\n      setDisplay(() => scrambleText(children));\n    }, speed);\n    timeoutRef.current = setTimeout(() => {\n      if (intervalRef.current) {\n        clearInterval(intervalRef.current);\n      }\n      setDisplay(children);\n    }, duration);\n  };\n\n  const handleMouseLeave = () => {\n    if (intervalRef.current) {\n      clearInterval(intervalRef.current);\n    }\n    if (timeoutRef.current) {\n      clearTimeout(timeoutRef.current);\n    }\n    setDisplay(children);\n  };\n\n  return (\n    <button\n      className={className}\n      onBlur={handleMouseLeave}\n      onFocus={isHoverDevice ? handleMouseEnter : undefined}\n      onMouseEnter={isHoverDevice ? handleMouseEnter : undefined}\n      onMouseLeave={handleMouseLeave}\n      style={{\n        background: \"none\",\n        border: \"none\",\n        color: \"inherit\",\n        cursor: \"pointer\",\n        display: \"inline-block\",\n        font: \"inherit\",\n        padding: 0,\n        textAlign: \"inherit\",\n      }}\n      type=\"button\"\n    >\n      {display}\n    </button>\n  );\n};\n\nexport default ScrambleHover;\n","path":"index.tsx","target":"components/smoothui/scramble-hover/index.tsx","type":"registry:ui"}],"name":"scramble-hover","registryDependencies":[],"title":"Scramble Hover","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"A ScrollRevealParagraph component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport {\n  type MotionValue,\n  motion,\n  useReducedMotion,\n  useScroll,\n  useTransform,\n} from \"motion/react\";\nimport { useRef } from \"react\";\n\nconst KEY_PREFIX_LENGTH = 3;\n\nexport interface ScrollRevealParagraphProps {\n  className?: string;\n  paragraph: string;\n}\n\nexport default function ScrollRevealParagraph({\n  paragraph,\n  className = \"\",\n}: ScrollRevealParagraphProps) {\n  const container = useRef<HTMLParagraphElement>(null);\n  const { scrollYProgress } = useScroll({\n    offset: [\"start 0.9\", \"start 0.25\"],\n    target: container,\n  });\n\n  const words = paragraph.split(\" \");\n\n  return (\n    <p className={`text-lg leading-relaxed ${className}`} ref={container}>\n      {words.map((word, i) => {\n        const start = i / words.length;\n        const end = start + 1 / words.length;\n        return (\n          <Word\n            key={`word-${i}-${word.slice(0, KEY_PREFIX_LENGTH)}`}\n            progress={scrollYProgress}\n            range={[start, end]}\n          >\n            {word}\n          </Word>\n        );\n      })}\n    </p>\n  );\n}\n\ninterface WordProps {\n  children: string;\n  progress: MotionValue<number>;\n  range: [number, number];\n}\n\nconst Word = ({ children, progress, range }: WordProps) => {\n  const shouldReduceMotion = useReducedMotion();\n  const opacity = useTransform(\n    progress,\n    range,\n    shouldReduceMotion ? [1, 1] : [0, 1]\n  );\n\n  return (\n    <span className=\"relative mr-2 inline-block\">\n      {shouldReduceMotion ? null : (\n        <span className=\"text-foreground/10\">{children}</span>\n      )}\n      <motion.span\n        className=\"absolute inset-0 text-foreground\"\n        style={{ opacity }}\n      >\n        {children}\n      </motion.span>\n    </span>\n  );\n};\n","path":"index.tsx","target":"components/smoothui/scroll-reveal-paragraph/index.tsx","type":"registry:ui"}],"name":"scroll-reveal-paragraph","registryDependencies":[],"title":"Scroll Reveal Paragraph","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"A ScrollableCardStack component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { motion, useMotionValue, useReducedMotion } from \"motion/react\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\n\nconst SCROLL_TIMEOUT_OFFSET = 100;\nconst MIN_SCROLL_INTERVAL = 300;\nconst SCROLL_THRESHOLD = 20;\nconst TOUCH_SCROLL_THRESHOLD = 100;\nconst SCALE_FACTOR = 0.08;\nconst MIN_SCALE = 0.08;\nconst MAX_SCALE = 2;\nconst HOVER_SCALE_MULTIPLIER = 1.02;\nconst CARD_PADDING = 100;\n\ninterface CardItem {\n  avatar: string;\n  handle: string;\n  href: string;\n  id: string;\n  image: string;\n  name: string;\n}\n\nexport interface ScrollableCardStackProps {\n  cardHeight?: number;\n  className?: string;\n  items: CardItem[];\n  perspective?: number;\n  transitionDuration?: number;\n}\n\nconst ScrollableCardStack: React.FC<ScrollableCardStackProps> = ({\n  items,\n  cardHeight = 384,\n  perspective = 1000,\n  transitionDuration = 180,\n  className,\n}) => {\n  const [currentIndex, setCurrentIndex] = useState(0);\n  const [isDragging, setIsDragging] = useState(false);\n  const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);\n  const [isScrolling, setIsScrolling] = useState(false);\n  const containerRef = useRef<HTMLDivElement>(null);\n  const scrollY = useMotionValue(0);\n  const lastScrollTime = useRef(0);\n  const shouldReduceMotion = useReducedMotion();\n\n  // Calculate the total number of items\n  const totalItems = items.length;\n  const maxIndex = totalItems - 1;\n\n  // Constants for visual effects - matching reference code exactly\n  const FRAME_OFFSET = -30;\n  const FRAMES_VISIBLE_LENGTH = 3;\n  const SNAP_DISTANCE = 50;\n\n  // Clamp function from reference code - memoized to prevent recreation\n  const clamp = useCallback(\n    (val: number, [min, max]: [number, number]): number =>\n      Math.min(Math.max(val, min), max),\n    []\n  );\n\n  // Controlled scroll function to move exactly one card\n  const scrollToCard = useCallback(\n    (direction: 1 | -1) => {\n      if (isScrolling) {\n        return;\n      }\n\n      const now = Date.now();\n      const timeSinceLastScroll = now - lastScrollTime.current;\n\n      if (timeSinceLastScroll < MIN_SCROLL_INTERVAL) {\n        return;\n      }\n\n      const newIndex = clamp(currentIndex + direction, [0, maxIndex]);\n\n      if (newIndex !== currentIndex) {\n        lastScrollTime.current = now;\n        setIsScrolling(true);\n        setCurrentIndex(newIndex);\n        scrollY.set(newIndex * SNAP_DISTANCE);\n\n        setTimeout(() => {\n          setIsScrolling(false);\n        }, transitionDuration + SCROLL_TIMEOUT_OFFSET);\n      }\n    },\n    [currentIndex, maxIndex, scrollY, isScrolling, transitionDuration, clamp]\n  );\n\n  // Handle scroll events with improved responsiveness\n  const handleScroll = useCallback(\n    (deltaY: number) => {\n      if (isDragging || isScrolling) {\n        return;\n      }\n\n      if (Math.abs(deltaY) < SCROLL_THRESHOLD) {\n        return;\n      }\n\n      const scrollDirection = deltaY > 0 ? 1 : -1;\n      scrollToCard(scrollDirection);\n    },\n    [isDragging, isScrolling, scrollToCard]\n  );\n\n  // Handle wheel events\n  const handleWheel = useCallback(\n    (e: WheelEvent) => {\n      e.preventDefault();\n      handleScroll(e.deltaY);\n    },\n    [handleScroll]\n  );\n\n  // Handle keyboard navigation - improved with reference code logic\n  const handleKeyDown = useCallback(\n    (e: React.KeyboardEvent) => {\n      if (isScrolling) {\n        return;\n      }\n\n      switch (e.key) {\n        case \"ArrowUp\":\n        case \"ArrowLeft\": {\n          e.preventDefault();\n          scrollToCard(-1);\n          break;\n        }\n        case \"ArrowDown\":\n        case \"ArrowRight\": {\n          e.preventDefault();\n          scrollToCard(1);\n          break;\n        }\n        case \"Home\": {\n          e.preventDefault();\n          if (currentIndex !== 0) {\n            setIsScrolling(true);\n            setCurrentIndex(0);\n            scrollY.set(0);\n            setTimeout(() => {\n              setIsScrolling(false);\n            }, transitionDuration + SCROLL_TIMEOUT_OFFSET);\n          }\n          break;\n        }\n        case \"End\": {\n          e.preventDefault();\n          if (currentIndex !== maxIndex) {\n            setIsScrolling(true);\n            setCurrentIndex(maxIndex);\n            scrollY.set(maxIndex * SNAP_DISTANCE);\n            setTimeout(() => {\n              setIsScrolling(false);\n            }, transitionDuration + SCROLL_TIMEOUT_OFFSET);\n          }\n          break;\n        }\n        default: {\n          // No action for other keys\n          break;\n        }\n      }\n    },\n    [\n      currentIndex,\n      maxIndex,\n      scrollY,\n      isScrolling,\n      scrollToCard,\n      transitionDuration,\n    ]\n  );\n\n  // Handle touch events for mobile\n  const touchStartY = useRef(0);\n  const touchStartIndex = useRef(0);\n  const touchStartTime = useRef(0);\n  const touchMoved = useRef(false);\n\n  const handleTouchStart = useCallback(\n    (e: React.TouchEvent) => {\n      touchStartY.current = e.touches[0].clientY;\n      touchStartIndex.current = currentIndex;\n      touchStartTime.current = Date.now();\n      touchMoved.current = false;\n      setIsDragging(true);\n    },\n    [currentIndex]\n  );\n\n  const handleTouchMove = useCallback(\n    (e: React.TouchEvent) => {\n      if (!isDragging || isScrolling) {\n        return;\n      }\n\n      const touchY = e.touches[0].clientY;\n      const deltaY = touchStartY.current - touchY;\n\n      if (Math.abs(deltaY) > TOUCH_SCROLL_THRESHOLD && !touchMoved.current) {\n        const scrollDirection = deltaY > 0 ? 1 : -1;\n        scrollToCard(scrollDirection);\n        touchMoved.current = true;\n      }\n    },\n    [isDragging, isScrolling, scrollToCard]\n  );\n\n  const handleTouchEnd = useCallback(() => {\n    setIsDragging(false);\n    touchMoved.current = false;\n  }, []);\n\n  // Set up event listeners\n  useEffect(() => {\n    const container = containerRef.current;\n    if (!container) {\n      return;\n    }\n\n    container.addEventListener(\"wheel\", handleWheel, { passive: false });\n\n    return () => {\n      container.removeEventListener(\"wheel\", handleWheel);\n    };\n  }, [handleWheel]);\n\n  // Snap to current index when not dragging\n  useEffect(() => {\n    if (!isDragging) {\n      scrollY.set(currentIndex * SNAP_DISTANCE);\n    }\n  }, [currentIndex, isDragging, scrollY]);\n\n  // Calculate transform for each card based on the reference code\n  const getCardTransform = useCallback(\n    (index: number) => {\n      const offsetIndex = index - currentIndex;\n\n      // Apply blur effect for cards behind the current one - matching reference exactly\n      const isBehindCurrent = currentIndex > index;\n      const blur = !shouldReduceMotion && isBehindCurrent ? 2 : 0;\n\n      // Opacity based on distance - improved logic from reference\n      const opacity = currentIndex > index ? 0 : 1;\n\n      // Scale with improved calculation inspired by reference - using clamp function\n      const scale = shouldReduceMotion\n        ? 1\n        : clamp(1 - offsetIndex * SCALE_FACTOR, [MIN_SCALE, MAX_SCALE]);\n\n      // Vertical offset with improved calculation - matching reference exactly\n      const y = shouldReduceMotion\n        ? 0\n        : clamp(offsetIndex * FRAME_OFFSET, [\n            FRAME_OFFSET * FRAMES_VISIBLE_LENGTH,\n            Number.POSITIVE_INFINITY,\n          ]);\n\n      // Z-index for proper layering - matching reference pattern\n      const zIndex = items.length - index;\n\n      return {\n        blur,\n        opacity,\n        scale,\n        y,\n        zIndex,\n      };\n    },\n    [currentIndex, items.length, clamp, shouldReduceMotion]\n  );\n\n  return (\n    <section\n      aria-atomic=\"true\"\n      aria-label=\"Scrollable card stack\"\n      aria-live=\"polite\"\n      className={cn(\"relative mx-auto h-fit w-fit min-w-[300px]\", className)}\n    >\n      {/* biome-ignore lint/a11y/noNoninteractiveElementInteractions: Interactive scrollable widget requires event handlers */}\n      <div\n        aria-label=\"Scrollable card container\"\n        className=\"h-full w-full\"\n        onKeyDown={handleKeyDown}\n        onTouchEnd={handleTouchEnd}\n        onTouchMove={handleTouchMove}\n        onTouchStart={handleTouchStart}\n        ref={containerRef}\n        role=\"application\"\n        style={{\n          minHeight: `${cardHeight + CARD_PADDING}px`, // Add some padding for the card stack effect\n          perspective: `${perspective}px`,\n          perspectiveOrigin: \"center 60%\",\n          touchAction: \"none\",\n        }}\n        // biome-ignore lint/a11y/noNoninteractiveTabindex: Required for keyboard navigation\n        tabIndex={0}\n      >\n        {items.map((item, i) => {\n          const transform = getCardTransform(i);\n          const isActive = i === currentIndex;\n          const isHovered = hoveredIndex === i;\n\n          return (\n            <motion.div\n              animate={\n                shouldReduceMotion\n                  ? { x: \"-50%\" }\n                  : {\n                      scale: transform.scale,\n                      x: \"-50%\",\n                      y: `calc(-50% + ${transform.y}px)`,\n                    }\n              }\n              aria-hidden={!isActive}\n              className=\"absolute top-1/2 left-1/2 w-max max-w-[100vw] overflow-hidden rounded-2xl border bg-background shadow-lg\"\n              data-active={isActive}\n              initial={false}\n              key={`scrollable-card-${item.id}`}\n              onBlur={() => setHoveredIndex(null)}\n              onFocus={() => isActive && setHoveredIndex(i)}\n              onMouseEnter={() => isActive && setHoveredIndex(i)}\n              onMouseLeave={() => setHoveredIndex(null)}\n              style={{\n                // Dynamic border width based on scale - from reference code\n                borderWidth: `${2 / transform.scale}px`,\n                filter: `blur(${transform.blur}px)`,\n                height: `${cardHeight}px`,\n                opacity: transform.opacity,\n                pointerEvents: isActive ? \"auto\" : \"none\",\n                transformOrigin: \"center center\",\n                transitionDuration: shouldReduceMotion ? \"0ms\" : \"200ms\",\n                transitionProperty: shouldReduceMotion\n                  ? \"none\"\n                  : \"opacity, filter\",\n                transitionTimingFunction:\n                  \"cubic-bezier(0.645, 0.045, 0.355, 1)\",\n                willChange: shouldReduceMotion\n                  ? undefined\n                  : \"opacity, filter, transform\",\n                zIndex: transform.zIndex,\n              }}\n              tabIndex={isActive ? 0 : -1}\n              transition={\n                shouldReduceMotion\n                  ? { duration: 0 }\n                  : {\n                      damping: 20,\n                      duration: 0.25,\n                      mass: 0.5,\n                      stiffness: 250,\n                      type: \"spring\" as const,\n                    }\n              }\n              whileHover={\n                shouldReduceMotion || !isActive\n                  ? {}\n                  : {\n                      scale: transform.scale * HOVER_SCALE_MULTIPLIER,\n                      transition: {\n                        damping: 20,\n                        duration: 0.25,\n                        mass: 0.5,\n                        stiffness: 250,\n                        type: \"spring\" as const,\n                      },\n                    }\n              }\n            >\n              {/* Card Content */}\n              <div\n                className={cn(\n                  \"flex aspect-16/10 w-full flex-col rounded-xl bg-background transition-all duration-200\",\n                  isHovered && \"shadow-xl\",\n                  isScrolling && isActive && \"ring-2 ring-brand ring-opacity-50\"\n                )}\n                style={{ height: `${cardHeight}px` }}\n              >\n                {/* Scroll indicator */}\n                {isScrolling && isActive ? (\n                  <div className=\"absolute -top-1 left-1/2 h-1 w-8 -translate-x-1/2 rounded-full bg-brand opacity-75\" />\n                ) : null}\n\n                {/* Image Container - takes remaining space */}\n                <div className=\"relative w-full flex-1 overflow-hidden\">\n                  {/* Background blur image */}\n                  <img\n                    alt=\"\"\n                    aria-hidden=\"true\"\n                    className=\"absolute inset-0 h-full w-full object-cover text-transparent\"\n                    decoding=\"async\"\n                    draggable={false}\n                    height={10}\n                    src=\"data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAiIGhlaWdodD0iMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHJlY3Qgd2lkdGg9IjEwIiBoZWlnaHQ9IjEwIiBmaWxsPSIjZjNmNGY2Ii8+PC9zdmc+\"\n                    style={{\n                      filter: \"blur(32px)\",\n                      pointerEvents: \"none\",\n                      scale: \"1.2\",\n                      zIndex: 1,\n                    }}\n                    width={10}\n                  />\n                  {/* Image */}\n                  <img\n                    alt={`${item.name}'s card`}\n                    className=\"absolute inset-0 h-full w-full object-cover\"\n                    decoding=\"async\"\n                    draggable={false}\n                    height={cardHeight}\n                    src={item.image}\n                    style={{\n                      pointerEvents: \"none\",\n                      userSelect: \"none\",\n                      zIndex: 2,\n                    }}\n                    width={400}\n                  />\n                </div>\n\n                {/* User Info - always at bottom */}\n                <a\n                  aria-label={`View ${item.name}'s profile`}\n                  className={cn(\n                    \"flex items-center justify-center gap-1 bg-background/95 p-3 text-decoration-none text-inherit backdrop-blur-sm transition-colors duration-200\"\n                  )}\n                  href={item.href}\n                  rel=\"noopener noreferrer\"\n                  target=\"_blank\"\n                >\n                  <img\n                    alt={`${item.name}'s avatar`}\n                    className=\"mr-1 h-5 w-5 overflow-hidden rounded-full\"\n                    draggable={false}\n                    height={20}\n                    src={item.avatar}\n                    style={{\n                      boxShadow: \"0 0 0 1px var(--border-secondary, #e0e0e0)\",\n                    }}\n                    width={20}\n                  />\n                  <span className=\"font-medium text-foreground text-sm leading-none\">\n                    {item.name}\n                  </span>\n                  <span className=\"font-normal text-foreground/70 text-sm\">\n                    {item.handle}\n                  </span>\n                </a>\n              </div>\n            </motion.div>\n          );\n        })}\n\n        {/* Navigation indicators */}\n        <div\n          aria-label=\"Card navigation\"\n          className=\"absolute bottom-4 left-1/2 flex -translate-x-1/2 transform space-x-2\"\n          role=\"tablist\"\n        >\n          {Array.from({ length: items.length }, (_, i) => (\n            <motion.button\n              aria-label={`Go to card ${i + 1} of ${items.length}`}\n              aria-selected={i === currentIndex}\n              className={cn(\n                \"h-2 w-2 cursor-pointer rounded-full transition-all duration-200 focus:outline-none focus:ring-1 focus:ring-brand focus:ring-offset-1\",\n                i === currentIndex\n                  ? \"scale-125 bg-brand\"\n                  : \"bg-gray-300 hover:bg-gray-400\"\n              )}\n              key={`scrollable-indicator-${items[i]?.id || i}`}\n              onClick={() => {\n                if (i !== currentIndex && !isScrolling) {\n                  setIsScrolling(true);\n                  setCurrentIndex(i);\n                  scrollY.set(i * SNAP_DISTANCE);\n                  setTimeout(() => {\n                    setIsScrolling(false);\n                  }, transitionDuration + SCROLL_TIMEOUT_OFFSET);\n                }\n              }}\n              role=\"tab\"\n              transition={{\n                damping: 20,\n                mass: 0.5,\n                stiffness: 250,\n                type: \"spring\" as const,\n              }}\n              type=\"button\"\n              whileHover={{ scale: 1.2 }}\n              whileTap={{ scale: 0.9 }}\n            />\n          ))}\n        </div>\n\n        {/* Instructions for screen readers */}\n        <div aria-live=\"polite\" className=\"sr-only\">\n          {`Card ${currentIndex + 1} of ${items.length} selected. Use arrow keys to navigate one card at a time, or click the dots below.`}\n        </div>\n      </div>\n    </section>\n  );\n};\n\nexport default ScrollableCardStack;\n","path":"index.tsx","target":"components/smoothui/scrollable-card-stack/index.tsx","type":"registry:ui"}],"name":"scrollable-card-stack","registryDependencies":["https://smoothui.dev/r/tokens.json"],"title":"Scrollable Card Stack","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"Design-tool style scrubber slider with animated thumb and built-in label","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\n\nexport interface ScrubberProps {\n  /** Additional CSS classes */\n  className?: string;\n  /** Number of decimal places to display */\n  decimals?: number;\n  /** Default value for uncontrolled usage */\n  defaultValue?: number;\n  /** Label displayed on the left side of the track */\n  label?: string;\n  /** Maximum value */\n  max?: number;\n  /** Minimum value */\n  min?: number;\n  /** Called when value changes during interaction */\n  onValueChange?: (value: number) => void;\n  /** Step increment */\n  step?: number;\n  /** Number of tick marks (0 to hide) */\n  ticks?: number;\n  /** Controlled value */\n  value?: number;\n}\n\nconst clamp = (val: number, min: number, max: number) =>\n  Math.min(Math.max(val, min), max);\n\nconst roundToStep = (val: number, step: number, min: number) =>\n  Math.round((val - min) / step) * step + min;\n\nconst Scrubber = ({\n  label = \"Value\",\n  value: controlledValue,\n  defaultValue = 0,\n  onValueChange,\n  min = 0,\n  max = 1,\n  step = 0.01,\n  decimals = 2,\n  ticks = 9,\n  className,\n}: ScrubberProps) => {\n  const shouldReduceMotion = useReducedMotion();\n  const trackRef = useRef<HTMLDivElement>(null);\n  const [internalValue, setInternalValue] = useState(defaultValue);\n  const [isDragging, setIsDragging] = useState(false);\n  const [isHovering, setIsHovering] = useState(false);\n  const [isHoverDevice, setIsHoverDevice] = useState(false);\n\n  const isControlled = controlledValue !== undefined;\n  const value = isControlled ? controlledValue : internalValue;\n  const range = max - min;\n  const percentage = range > 0 ? ((value - min) / range) * 100 : 0;\n  const isActive = isDragging || (isHoverDevice && isHovering);\n\n  useEffect(() => {\n    const mq = window.matchMedia(\"(hover: hover) and (pointer: fine)\");\n    setIsHoverDevice(mq.matches);\n    const onChange = (e: MediaQueryListEvent) => setIsHoverDevice(e.matches);\n    mq.addEventListener(\"change\", onChange);\n    return () => mq.removeEventListener(\"change\", onChange);\n  }, []);\n\n  const setValue = useCallback(\n    (newValue: number) => {\n      const clamped = clamp(roundToStep(newValue, step, min), min, max);\n      if (!isControlled) {\n        setInternalValue(clamped);\n      }\n      onValueChange?.(clamped);\n    },\n    [step, min, max, isControlled, onValueChange]\n  );\n\n  const getValueFromPointer = useCallback(\n    (clientX: number) => {\n      const track = trackRef.current;\n      if (!track) {\n        return value;\n      }\n      const rect = track.getBoundingClientRect();\n      const ratio = clamp((clientX - rect.left) / rect.width, 0, 1);\n      return min + ratio * range;\n    },\n    [min, range, value]\n  );\n\n  const handlePointerDown = useCallback(\n    (e: React.PointerEvent) => {\n      e.preventDefault();\n      trackRef.current?.setPointerCapture(e.pointerId);\n      setIsDragging(true);\n      setValue(getValueFromPointer(e.clientX));\n    },\n    [getValueFromPointer, setValue]\n  );\n\n  const handlePointerMove = useCallback(\n    (e: React.PointerEvent) => {\n      if (!isDragging) {\n        return;\n      }\n      setValue(getValueFromPointer(e.clientX));\n    },\n    [isDragging, getValueFromPointer, setValue]\n  );\n\n  const handlePointerUp = useCallback(() => {\n    setIsDragging(false);\n  }, []);\n\n  const handleKeyDown = useCallback(\n    (e: React.KeyboardEvent) => {\n      let next: number | undefined;\n      switch (e.key) {\n        case \"ArrowRight\":\n        case \"ArrowUp\":\n          next = value + step;\n          break;\n        case \"ArrowLeft\":\n        case \"ArrowDown\":\n          next = value - step;\n          break;\n        case \"Home\":\n          next = min;\n          break;\n        case \"End\":\n          next = max;\n          break;\n        default:\n          return;\n      }\n      e.preventDefault();\n      setValue(next);\n    },\n    [value, step, min, max, setValue]\n  );\n\n  const springConfig = shouldReduceMotion\n    ? { duration: 0 }\n    : { bounce: 0.1, duration: 0.25, type: \"spring\" as const };\n\n  return (\n    <div className={cn(\"relative w-full select-none\", className)}>\n      <div\n        aria-label={label}\n        aria-valuemax={max}\n        aria-valuemin={min}\n        aria-valuenow={Number(value.toFixed(decimals))}\n        className=\"relative cursor-pointer overflow-hidden bg-muted outline-offset-2\"\n        onKeyDown={handleKeyDown}\n        onMouseEnter={() => setIsHovering(true)}\n        onMouseLeave={() => setIsHovering(false)}\n        onPointerDown={handlePointerDown}\n        onPointerMove={handlePointerMove}\n        onPointerUp={handlePointerUp}\n        ref={trackRef}\n        role=\"slider\"\n        style={{\n          borderRadius: 8,\n          height: 36,\n          touchAction: \"none\",\n        }}\n        tabIndex={0}\n      >\n        {/* Fill indicator */}\n        <div\n          className=\"pointer-events-none absolute inset-y-0 left-0 bg-foreground/14\"\n          style={{\n            borderRadius: 8,\n            transition: isDragging\n              ? \"none\"\n              : \"width 150ms cubic-bezier(0.23, 1, 0.32, 1)\",\n            width: `${percentage}%`,\n          }}\n        />\n\n        {/* Tick marks */}\n        {ticks > 0 && (\n          <div className=\"pointer-events-none absolute inset-0\">\n            {Array.from({ length: ticks }, (_, i) => {\n              const pos = ((i + 1) / (ticks + 1)) * 100;\n              return (\n                <div\n                  className=\"absolute top-1/2 bg-foreground/25\"\n                  key={pos}\n                  style={{\n                    borderRadius: 999,\n                    height: 6,\n                    left: `${pos}%`,\n                    transform: \"translateX(-50%) translateY(-50%)\",\n                    width: 1,\n                  }}\n                />\n              );\n            })}\n          </div>\n        )}\n\n        {/* Scrub bar (capsule thumb) */}\n        <div\n          className=\"pointer-events-none absolute\"\n          style={{\n            left: `${percentage}%`,\n            marginLeft: -5,\n            top: \"50%\",\n            transform: \"translateX(-50%) translateY(-50%)\",\n            transition: isDragging\n              ? \"none\"\n              : \"left 150ms cubic-bezier(0.23, 1, 0.32, 1)\",\n            zIndex: 3,\n          }}\n        >\n          <motion.div\n            animate={{\n              opacity: isActive ? 0.8 : 0.15,\n              scaleX: isActive ? 1 : 0.7,\n              scaleY: isActive ? 1 : 0.7,\n            }}\n            className=\"bg-foreground/90\"\n            style={{\n              borderRadius: 999,\n              height: 22,\n              width: 4,\n            }}\n            transition={springConfig}\n          />\n        </div>\n\n        {/* Label */}\n        <div\n          className=\"pointer-events-none absolute top-1/2 left-3 -translate-y-1/2 whitespace-nowrap text-foreground\"\n          style={{\n            fontSize: 13,\n            fontWeight: 500,\n            zIndex: 4,\n          }}\n        >\n          {label}\n        </div>\n\n        {/* Value display */}\n        <div\n          className=\"pointer-events-none absolute top-1/2 right-2.5 -translate-y-1/2 text-foreground\"\n          style={{\n            fontFamily: \"ui-monospace, monospace\",\n            fontSize: 13,\n            fontVariantNumeric: \"tabular-nums\",\n            fontWeight: 500,\n            zIndex: 4,\n          }}\n        >\n          {value.toFixed(decimals)}\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default Scrubber;\n","path":"index.tsx","target":"components/smoothui/scrubber/index.tsx","type":"registry:ui"}],"name":"scrubber","registryDependencies":[],"title":"Scrubber","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"A persistent WebGL SDF blob transition wrapper for route and state changes.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { useReducedMotion } from \"motion/react\";\nimport { type ReactNode, useEffect, useRef, useState } from \"react\";\n\nexport type SdfBlobVariant = \"circle\" | \"warped\" | \"merge\" | \"radial\" | \"final\";\n\nexport interface SdfBlobTransitionProps {\n  children: ReactNode;\n  className?: string;\n  duration?: number;\n  onRest?: () => void;\n  transitionKey: string | number;\n  variant?: SdfBlobVariant;\n}\n\nconst DEFAULT_DURATION_MS = 1120;\nconst MIDPOINT = 0.5;\nconst MAX_DPR = 2;\nconst VERTEX_SHADER =\n  \"attribute vec2 a; void main(){ gl_Position = vec4(a, 0.0, 1.0); }\";\nconst FRAGMENT_SHADER = `\nprecision mediump float;\nuniform vec2 uRes;\nuniform float uTime;\nuniform float uProgress;\nuniform float uAlpha;\nuniform float uVariant;\n#define PI 3.14159265359\n\nfloat hash(vec2 p){ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123); }\nfloat noise(vec2 p){\n  vec2 i = floor(p);\n  vec2 f = fract(p);\n  float a = hash(i);\n  float b = hash(i + vec2(1.0, 0.0));\n  float c = hash(i + vec2(0.0, 1.0));\n  float d = hash(i + vec2(1.0, 1.0));\n  vec2 u = f * f * (3.0 - 2.0 * f);\n  return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y;\n}\n\nfloat circleSdf(vec2 p, vec2 c, float r){ return length(p - c) - r; }\nfloat smin(float a, float b, float k){\n  float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);\n  return mix(b, a, h) - k * h * (1.0 - h);\n}\n\nvoid main(){\n  vec2 uv = gl_FragCoord.xy / uRes;\n  vec2 aspect = vec2(uRes.x / uRes.y, 1.0);\n  vec2 p = (uv - 0.5) * aspect;\n  float progress = smoothstep(0.0, 1.0, uProgress);\n  float grow = smoothstep(0.0, 0.86, progress);\n  float exitFade = smoothstep(1.0, 0.72, progress);\n\n  float wobble = noise(uv * 7.0 + uTime * 0.18) * 0.035;\n  float ringNoise = noise(vec2(atan(p.y, p.x) * 2.0, uTime * 0.18)) * 0.075;\n  float dCircle = circleSdf(p, vec2(0.0), 0.04 + grow * 0.64);\n  float dWarped = circleSdf(p, vec2(0.0), 0.04 + grow * 0.62 + ringNoise * smoothstep(0.08, 0.96, progress));\n\n  float m0 = circleSdf(p, vec2(-0.26, -0.08), 0.06 + grow * 0.42 + wobble);\n  float m1 = circleSdf(p, vec2(0.24, 0.12), 0.06 + grow * 0.40 - wobble * 0.5);\n  float dMerge = smin(m0, m1, 0.18);\n\n  float angle = atan(p.y, p.x);\n  float sector3 = 2.0 * PI / 3.0;\n  float sector5 = 2.0 * PI / 5.0;\n  float a3 = floor((angle + PI) / sector3 + 0.5) * sector3 - PI;\n  float a5 = floor((angle + PI) / sector5 + 0.5) * sector5 - PI;\n  float r0 = 0.17 + grow * 0.24;\n  float r1 = 0.30 + grow * 0.18;\n  float dR0 = circleSdf(p, vec2(cos(a3), sin(a3)) * r0, 0.04 + grow * 0.22 + wobble * 0.4);\n  float dR1 = circleSdf(p, vec2(cos(a5), sin(a5)) * r1, 0.035 + grow * 0.18);\n  float dRadial = smin(dR0, dR1, 0.14);\n\n  float f0 = circleSdf(p, vec2(-0.28, -0.10), 0.08 + grow * 0.46 + wobble);\n  float f1 = circleSdf(p, vec2(0.00, 0.16), 0.06 + grow * 0.42 - wobble * 0.5);\n  float f2 = circleSdf(p, vec2(0.30, -0.06), 0.05 + grow * 0.40 + wobble * 0.6);\n  float f3 = circleSdf(p, vec2(-0.02, -0.30), 0.04 + grow * 0.34);\n  float dFinal = smin(smin(f0, f1, 0.18), smin(f2, f3, 0.16), 0.20);\n  dFinal = smin(dFinal, dRadial, 0.16);\n\n  float d = dFinal;\n  if (uVariant < 0.5) {\n    d = dCircle;\n  } else if (uVariant < 1.5) {\n    d = dWarped;\n  } else if (uVariant < 2.5) {\n    d = dMerge;\n  } else if (uVariant < 3.5) {\n    d = dRadial;\n  }\n\n  float edge = exp(-d * d * 160.0) * exitFade;\n  float fill = smoothstep(0.08, -0.10, d) * smoothstep(0.0, 0.18, progress) * exitFade;\n  float cell = hash(floor((uv + noise(uv * 11.0) * 0.01) * uRes / 7.0) + floor(uTime * 10.0));\n  float flecks = step(0.78, cell) * edge * 0.22 * smoothstep(1.5, 4.0, uVariant);\n\n  vec3 rose = vec3(1.0, 0.23, 0.58);\n  vec3 apricot = vec3(1.0, 0.70, 0.38);\n  vec3 mint = vec3(0.42, 0.94, 0.78);\n  vec3 color = mix(rose, apricot, smoothstep(-0.35, 0.35, p.x + noise(uv * 5.0) * 0.18));\n  color = mix(color, mint, smoothstep(0.12, 0.7, p.y + grow * 0.16));\n\n  float alpha = (fill * 0.24 + edge * 0.72 + flecks) * uAlpha;\n  gl_FragColor = vec4(color * alpha + vec3(1.0) * edge * 0.18 * uAlpha, clamp(alpha, 0.0, 0.9));\n}\n`;\nconst STRIP = new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]);\n\ninterface Playback {\n  cancel: () => void;\n}\ninterface Controller {\n  destroy: () => void;\n  setAlpha: (alpha: number) => void;\n  setProgress: (progress: number) => void;\n  setVariant: (variant: SdfBlobVariant) => void;\n}\n\nconst clamp01 = (value: number) => Math.max(0, Math.min(1, value));\nconst easeOutQuart = (value: number) => 1 - (1 - value) ** 4;\nconst easeInOutCubic = (value: number) =>\n  value < 0.5 ? 4 * value ** 3 : 1 - (-2 * value + 2) ** 3 / 2;\n\nconst compile = (gl: WebGLRenderingContext, type: number, source: string) => {\n  const shader = gl.createShader(type);\n  if (!shader) {\n    return null;\n  }\n  gl.shaderSource(shader, source);\n  gl.compileShader(shader);\n  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n    gl.deleteShader(shader);\n    return null;\n  }\n  return shader;\n};\n\nconst resize = (canvas: HTMLCanvasElement, gl: WebGLRenderingContext) => {\n  const rect = canvas.getBoundingClientRect();\n  const dpr = Math.min(window.devicePixelRatio || 1, MAX_DPR);\n  const width = Math.max(1, Math.round(rect.width * dpr));\n  const height = Math.max(1, Math.round(rect.height * dpr));\n  if (canvas.width !== width || canvas.height !== height) {\n    canvas.width = width;\n    canvas.height = height;\n  }\n  gl.viewport(0, 0, width, height);\n};\n\nconst variantToUniform = (variant: SdfBlobVariant) => {\n  switch (variant) {\n    case \"circle\":\n      return 0;\n    case \"warped\":\n      return 1;\n    case \"merge\":\n      return 2;\n    case \"radial\":\n      return 3;\n    default:\n      return 4;\n  }\n};\n\nconst createController = (\n  canvas: HTMLCanvasElement,\n  initialVariant: SdfBlobVariant\n): Controller | null => {\n  const gl = canvas.getContext(\"webgl\", {\n    alpha: true,\n    antialias: false,\n    premultipliedAlpha: true,\n  });\n  if (!gl) {\n    return null;\n  }\n  const vertex = compile(gl, gl.VERTEX_SHADER, VERTEX_SHADER);\n  const fragment = compile(gl, gl.FRAGMENT_SHADER, FRAGMENT_SHADER);\n  const program = gl.createProgram();\n  if (!(vertex && fragment && program)) {\n    return null;\n  }\n  gl.attachShader(program, vertex);\n  gl.attachShader(program, fragment);\n  gl.linkProgram(program);\n  gl.deleteShader(vertex);\n  gl.deleteShader(fragment);\n  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n    gl.deleteProgram(program);\n    return null;\n  }\n\n  // biome-ignore lint/correctness/useHookAtTopLevel: WebGLRenderingContext.useProgram is not a React hook.\n  gl.useProgram(program);\n  const buffer = gl.createBuffer();\n  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);\n  gl.bufferData(gl.ARRAY_BUFFER, STRIP, gl.STATIC_DRAW);\n  const position = gl.getAttribLocation(program, \"a\");\n  gl.enableVertexAttribArray(position);\n  gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0);\n  gl.enable(gl.BLEND);\n  gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);\n\n  const uRes = gl.getUniformLocation(program, \"uRes\");\n  const uTime = gl.getUniformLocation(program, \"uTime\");\n  const uProgress = gl.getUniformLocation(program, \"uProgress\");\n  const uAlpha = gl.getUniformLocation(program, \"uAlpha\");\n  const uVariant = gl.getUniformLocation(program, \"uVariant\");\n  const startedAt = performance.now();\n  let progress = 0;\n  let alpha = 0;\n  let variant = initialVariant;\n  let frame = 0;\n  let destroyed = false;\n\n  const tick = () => {\n    if (destroyed) {\n      return;\n    }\n    resize(canvas, gl);\n    gl.clearColor(0, 0, 0, 0);\n    gl.clear(gl.COLOR_BUFFER_BIT);\n    gl.uniform2f(uRes, canvas.width, canvas.height);\n    gl.uniform1f(uTime, (performance.now() - startedAt) / 1000);\n    gl.uniform1f(uProgress, progress);\n    gl.uniform1f(uAlpha, alpha);\n    gl.uniform1f(uVariant, variantToUniform(variant));\n    gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);\n    frame = requestAnimationFrame(tick);\n  };\n  frame = requestAnimationFrame(tick);\n\n  return {\n    destroy: () => {\n      destroyed = true;\n      cancelAnimationFrame(frame);\n      gl.clear(gl.COLOR_BUFFER_BIT);\n      if (buffer) {\n        gl.deleteBuffer(buffer);\n      }\n      gl.deleteProgram(program);\n    },\n    setAlpha: (nextAlpha: number) => {\n      alpha = clamp01(nextAlpha);\n    },\n    setProgress: (nextProgress: number) => {\n      progress = clamp01(nextProgress);\n    },\n    setVariant: (nextVariant: SdfBlobVariant) => {\n      variant = nextVariant;\n    },\n  };\n};\n\nconst play = (\n  controller: Controller,\n  duration: number,\n  onMidpoint: () => void,\n  onComplete: () => void\n): Playback => {\n  let frame = 0;\n  let cancelled = false;\n  let didSwap = false;\n  const startedAt = performance.now();\n  controller.setAlpha(1);\n\n  const advance = () => {\n    if (cancelled) {\n      return;\n    }\n    const raw = clamp01((performance.now() - startedAt) / duration);\n    const progress = easeOutQuart(raw);\n    controller.setProgress(progress);\n    if (!(didSwap || progress < MIDPOINT)) {\n      didSwap = true;\n      onMidpoint();\n    }\n    if (raw < 1) {\n      frame = requestAnimationFrame(advance);\n      return;\n    }\n    if (!didSwap) {\n      onMidpoint();\n    }\n    const fadeStartedAt = performance.now();\n    const fade = () => {\n      if (cancelled) {\n        return;\n      }\n      const fadeRaw = clamp01((performance.now() - fadeStartedAt) / 90);\n      controller.setAlpha(1 - easeInOutCubic(fadeRaw));\n      if (fadeRaw < 1) {\n        frame = requestAnimationFrame(fade);\n        return;\n      }\n      controller.setAlpha(0);\n      controller.setProgress(0);\n      onComplete();\n    };\n    frame = requestAnimationFrame(fade);\n  };\n  frame = requestAnimationFrame(advance);\n\n  return {\n    cancel: () => {\n      cancelled = true;\n      cancelAnimationFrame(frame);\n    },\n  };\n};\n\nconst SdfBlobTransition = ({\n  children,\n  className,\n  duration = DEFAULT_DURATION_MS,\n  onRest,\n  transitionKey,\n  variant = \"final\",\n}: SdfBlobTransitionProps) => {\n  const shouldReduceMotion = useReducedMotion();\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const controllerRef = useRef<Controller | null>(null);\n  const playbackRef = useRef<Playback | null>(null);\n  const previousKey = useRef(transitionKey);\n  const [renderedChildren, setRenderedChildren] = useState(children);\n  const [isActive, setIsActive] = useState(false);\n\n  useEffect(() => {\n    const canvas = canvasRef.current;\n    if (!canvas) {\n      return;\n    }\n    const controller = createController(canvas, variant);\n    controllerRef.current = controller;\n    return () => {\n      playbackRef.current?.cancel();\n      controller?.destroy();\n      controllerRef.current = null;\n    };\n  }, [variant]);\n\n  useEffect(() => {\n    if (previousKey.current === transitionKey) {\n      setRenderedChildren(children);\n      return;\n    }\n    previousKey.current = transitionKey;\n    playbackRef.current?.cancel();\n\n    if (shouldReduceMotion) {\n      setRenderedChildren(children);\n      setIsActive(false);\n      onRest?.();\n      return;\n    }\n\n    const controller = controllerRef.current;\n    if (!controller) {\n      setRenderedChildren(children);\n      onRest?.();\n      return;\n    }\n    controller.setVariant(variant);\n    setIsActive(true);\n    playbackRef.current = play(\n      controller,\n      duration,\n      () => setRenderedChildren(children),\n      () => {\n        setIsActive(false);\n        playbackRef.current = null;\n        onRest?.();\n      }\n    );\n  }, [children, duration, onRest, shouldReduceMotion, transitionKey, variant]);\n\n  return (\n    <div\n      className={cn(\n        \"relative isolate overflow-hidden rounded-2xl bg-background text-foreground\",\n        className\n      )}\n      data-transitioning={isActive ? \"true\" : \"false\"}\n    >\n      <div className=\"relative z-0\">{renderedChildren}</div>\n      <canvas\n        className={cn(\n          \"pointer-events-none absolute inset-0 z-10 h-full w-full transition-opacity duration-75\",\n          isActive && !shouldReduceMotion ? \"opacity-100\" : \"opacity-0\"\n        )}\n        ref={canvasRef}\n      />\n    </div>\n  );\n};\n\nexport default SdfBlobTransition;\n","path":"index.tsx","target":"components/smoothui/sdf-blob-transition/index.tsx","type":"registry:ui"}],"name":"sdf-blob-transition","registryDependencies":[],"title":"Sdf Blob Transition","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":[],"description":"A clean SDF circle reveal based on the first Codrops shader step.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport SdfBlobTransition from \"../sdf-blob-transition\";\n\nexport interface SdfCircleTransitionProps {\n  children: ReactNode;\n  className?: string;\n  duration?: number;\n  onRest?: () => void;\n  transitionKey: string | number;\n}\n\nconst SdfCircleTransition = ({\n  children,\n  className,\n  duration = 1120,\n  onRest,\n  transitionKey,\n}: SdfCircleTransitionProps) => (\n  <SdfBlobTransition\n    className={className}\n    duration={duration}\n    onRest={onRest}\n    transitionKey={transitionKey}\n    variant=\"circle\"\n  >\n    {children}\n  </SdfBlobTransition>\n);\n\nexport default SdfCircleTransition;\n","path":"index.tsx","target":"components/smoothui/sdf-circle-transition/index.tsx","type":"registry:ui"}],"name":"sdf-circle-transition","registryDependencies":["https://smoothui.dev/r/sdf-blob-transition.json"],"title":"Sdf Circle Transition","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion","lucide-react"],"description":"A SearchableDropdown component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { ChevronDown, Search, X } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport { createPortal } from \"react-dom\";\n\nconst ROTATION_ANGLE_OPEN = 180;\n\nexport interface SearchableDropdownItem {\n  description?: string;\n  icon?: React.ReactNode;\n  id: string | number;\n  label: string;\n}\n\nexport interface SearchableDropdownProps {\n  className?: string;\n  emptyMessage?: string;\n  items: SearchableDropdownItem[];\n  label: string;\n  onChange?: (item: SearchableDropdownItem) => void;\n  placeholder?: string;\n}\n\nexport default function SearchableDropdown({\n  label,\n  items,\n  onChange,\n  placeholder = \"Search...\",\n  emptyMessage = \"No results found\",\n  className = \"\",\n}: SearchableDropdownProps) {\n  const [isOpen, setIsOpen] = useState(false);\n  const [selectedItem, setSelectedItem] =\n    useState<SearchableDropdownItem | null>(null);\n  const [searchQuery, setSearchQuery] = useState(\"\");\n  const dropdownRef = useRef<HTMLDivElement>(null);\n  const buttonRef = useRef<HTMLButtonElement>(null);\n  const portalRef = useRef<HTMLDivElement>(null);\n  const inputRef = useRef<HTMLInputElement>(null);\n  const [position, setPosition] = useState({ left: 0, top: 0, width: 0 });\n  const shouldReduceMotion = useReducedMotion();\n\n  const filteredItems = useMemo(() => {\n    const trimmedQuery = searchQuery.trim();\n    if (!trimmedQuery) {\n      return items;\n    }\n\n    // Cache lowercase query to avoid repeated calls\n    const query = trimmedQuery.toLowerCase();\n    const itemsLength = items.length;\n    const results: typeof items = [];\n\n    // Early exit optimization: use for loop instead of filter for better performance\n    for (let i = 0; i < itemsLength; i++) {\n      const item = items[i];\n      const itemLabel = item.label.toLowerCase();\n      const description = item.description?.toLowerCase();\n\n      if (itemLabel.includes(query) || description?.includes(query)) {\n        results.push(item);\n      }\n    }\n\n    return results;\n  }, [items, searchQuery]);\n\n  const handleItemSelect = (item: SearchableDropdownItem) => {\n    setSelectedItem(item);\n    setIsOpen(false);\n    setSearchQuery(\"\");\n    onChange?.(item);\n  };\n\n  const handleClearSearch = () => {\n    setSearchQuery(\"\");\n    inputRef.current?.focus();\n  };\n\n  const handleToggle = () => {\n    if (!isOpen && buttonRef.current) {\n      const rect = buttonRef.current.getBoundingClientRect();\n      setPosition({\n        left: rect.left,\n        top: rect.bottom + 4,\n        width: rect.width,\n      });\n    }\n    setIsOpen(!isOpen);\n    if (isOpen) {\n      setSearchQuery(\"\");\n    } else {\n      setTimeout(() => inputRef.current?.focus(), 100);\n    }\n  };\n\n  // Update position on scroll/resize when open\n  useEffect(() => {\n    if (!(isOpen && buttonRef.current)) {\n      return;\n    }\n\n    const updatePosition = () => {\n      if (buttonRef.current) {\n        const rect = buttonRef.current.getBoundingClientRect();\n        setPosition({\n          left: rect.left,\n          top: rect.bottom + 4,\n          width: rect.width,\n        });\n      }\n    };\n\n    window.addEventListener(\"scroll\", updatePosition, true);\n    window.addEventListener(\"resize\", updatePosition);\n\n    return () => {\n      window.removeEventListener(\"scroll\", updatePosition, true);\n      window.removeEventListener(\"resize\", updatePosition);\n    };\n  }, [isOpen]);\n\n  // Close dropdown when clicking outside\n  useEffect(() => {\n    const handleClickOutside = (event: MouseEvent) => {\n      const target = event.target as Node;\n      if (\n        isOpen &&\n        dropdownRef.current &&\n        !dropdownRef.current.contains(target) &&\n        portalRef.current &&\n        !portalRef.current.contains(target)\n      ) {\n        setIsOpen(false);\n        setSearchQuery(\"\");\n      }\n    };\n\n    if (isOpen) {\n      document.addEventListener(\"mousedown\", handleClickOutside);\n    }\n    return () => {\n      document.removeEventListener(\"mousedown\", handleClickOutside);\n    };\n  }, [isOpen]);\n\n  // Keyboard navigation with arrow keys, enter, and escape\n  const [focusedIndex, setFocusedIndex] = useState(-1);\n\n  useEffect(() => {\n    const handleKeyDown = (event: KeyboardEvent) => {\n      if (!isOpen) {\n        // Open dropdown on Enter or Space when button is focused\n        if (\n          (event.key === \"Enter\" || event.key === \" \") &&\n          document.activeElement === buttonRef.current\n        ) {\n          event.preventDefault();\n          handleToggle();\n        }\n        return;\n      }\n\n      if (event.key === \"Escape\") {\n        setIsOpen(false);\n        setSearchQuery(\"\");\n        setFocusedIndex(-1);\n        buttonRef.current?.focus();\n      } else if (event.key === \"ArrowDown\") {\n        event.preventDefault();\n        setFocusedIndex((prev) =>\n          prev < filteredItems.length - 1 ? prev + 1 : 0\n        );\n      } else if (event.key === \"ArrowUp\") {\n        event.preventDefault();\n        setFocusedIndex((prev) =>\n          prev > 0 ? prev - 1 : filteredItems.length - 1\n        );\n      } else if (event.key === \"Enter\" && focusedIndex >= 0) {\n        event.preventDefault();\n        const item = filteredItems[focusedIndex];\n        if (item) {\n          handleItemSelect(item);\n        }\n      } else if (event.key === \"Home\") {\n        event.preventDefault();\n        setFocusedIndex(0);\n      } else if (event.key === \"End\") {\n        event.preventDefault();\n        setFocusedIndex(filteredItems.length - 1);\n      }\n    };\n\n    document.addEventListener(\"keydown\", handleKeyDown);\n    return () => document.removeEventListener(\"keydown\", handleKeyDown);\n    // biome-ignore lint/correctness/useExhaustiveDependencies: Handlers are stable via closure\n  }, [isOpen, filteredItems, focusedIndex, handleItemSelect, handleToggle]);\n\n  // Reset focused index when items change\n  useEffect(() => {\n    setFocusedIndex(-1);\n  }, []);\n\n  const dropdownContent = (\n    <AnimatePresence>\n      {isOpen ? (\n        <div ref={portalRef}>\n          <motion.div\n            animate={\n              shouldReduceMotion\n                ? { opacity: 1 }\n                : { opacity: 1, scaleY: 1, y: 0 }\n            }\n            className=\"fixed z-50 origin-top overflow-hidden rounded-lg border bg-background/95 shadow-lg backdrop-blur-md\"\n            exit={\n              shouldReduceMotion\n                ? { opacity: 0, transition: { duration: 0 } }\n                : {\n                    opacity: 0,\n                    scaleY: 0.8,\n                    transition: { duration: 0.15 },\n                    y: -10,\n                  }\n            }\n            initial={\n              shouldReduceMotion\n                ? { opacity: 1 }\n                : { opacity: 0, scaleY: 0.8, y: -10 }\n            }\n            style={{\n              left: `${position.left}px`,\n              top: `${position.top}px`,\n              width: `${position.width}px`,\n            }}\n            transition={\n              shouldReduceMotion\n                ? { duration: 0 }\n                : {\n                    damping: 30,\n                    duration: 0.25,\n                    mass: 0.8,\n                    stiffness: 400,\n                    type: \"spring\" as const,\n                  }\n            }\n          >\n            {/* Search Input */}\n            <div className=\"relative border-b p-2\">\n              <motion.div\n                animate={\n                  shouldReduceMotion ? { opacity: 1 } : { opacity: 1, x: 0 }\n                }\n                className=\"relative\"\n                initial={\n                  shouldReduceMotion ? { opacity: 1 } : { opacity: 0, x: -10 }\n                }\n                transition={\n                  shouldReduceMotion\n                    ? { duration: 0 }\n                    : {\n                        damping: 25,\n                        delay: 0.05,\n                        duration: 0.2,\n                        stiffness: 400,\n                        type: \"spring\" as const,\n                      }\n                }\n              >\n                <Search className=\"absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-muted-foreground\" />\n                <input\n                  aria-autocomplete=\"list\"\n                  aria-controls=\"dropdown-items\"\n                  aria-expanded={isOpen}\n                  aria-label=\"Search dropdown items\"\n                  className=\"w-full rounded-md border bg-transparent py-2 pr-8 pl-9 text-sm outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\"\n                  onChange={(e) => {\n                    setSearchQuery(e.target.value);\n                    setFocusedIndex(-1);\n                  }}\n                  placeholder={placeholder}\n                  ref={inputRef}\n                  role=\"combobox\"\n                  type=\"text\"\n                  value={searchQuery}\n                />\n                <AnimatePresence>\n                  {searchQuery ? (\n                    <motion.button\n                      animate={{ opacity: 1 }}\n                      aria-label=\"Clear search\"\n                      className=\"absolute top-1/2 right-2 min-h-[44px] min-w-[44px] -translate-y-1/2 rounded-full p-2 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\"\n                      exit={{ opacity: 0 }}\n                      initial={{ opacity: 0 }}\n                      onClick={handleClearSearch}\n                      transition={{\n                        damping: 25,\n                        stiffness: 400,\n                        type: \"spring\" as const,\n                      }}\n                      type=\"button\"\n                    >\n                      <X aria-hidden=\"true\" className=\"h-4 w-4\" />\n                    </motion.button>\n                  ) : null}\n                </AnimatePresence>\n              </motion.div>\n            </div>\n\n            {/* Items List */}\n            <ul\n              aria-label=\"Dropdown options\"\n              className=\"max-h-60 overflow-y-auto py-2\"\n              id=\"dropdown-items\"\n            >\n              <AnimatePresence mode=\"popLayout\">\n                {filteredItems.length > 0 ? (\n                  filteredItems.map((item, index) => (\n                    <motion.li\n                      animate={\n                        shouldReduceMotion\n                          ? { opacity: 1 }\n                          : { filter: \"blur(0px)\", opacity: 1, x: 0 }\n                      }\n                      aria-selected={\n                        selectedItem?.id === item.id || index === focusedIndex\n                      }\n                      className=\"block\"\n                      exit={\n                        shouldReduceMotion\n                          ? { opacity: 0, transition: { duration: 0 } }\n                          : { filter: \"blur(4px)\", opacity: 0, x: -10 }\n                      }\n                      initial={\n                        shouldReduceMotion\n                          ? { opacity: 1 }\n                          : { filter: \"blur(4px)\", opacity: 0, x: -10 }\n                      }\n                      key={item.id}\n                      layout\n                      role=\"option\"\n                      transition={\n                        shouldReduceMotion\n                          ? { duration: 0 }\n                          : {\n                              damping: 28,\n                              delay: index * 0.02,\n                              duration: 0.2,\n                              mass: 0.6,\n                              stiffness: 400,\n                              type: \"spring\" as const,\n                            }\n                      }\n                    >\n                      <button\n                        aria-label={`${item.label}${item.description ? `, ${item.description}` : \"\"}`}\n                        className={`flex min-h-[44px] w-full items-center px-4 py-2 text-left text-sm transition-colors hover:bg-muted focus-visible:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ${\n                          selectedItem?.id === item.id\n                            ? \"font-medium text-brand\"\n                            : \"\"\n                        } ${index === focusedIndex ? \"bg-muted\" : \"\"}`}\n                        onClick={() => handleItemSelect(item)}\n                        onMouseEnter={() => setFocusedIndex(index)}\n                        type=\"button\"\n                      >\n                        {item.icon ? (\n                          <span className=\"mr-3 shrink-0\">{item.icon}</span>\n                        ) : null}\n                        <div className=\"min-w-0 flex-1\">\n                          <span className=\"block truncate\">{item.label}</span>\n                          {item.description ? (\n                            <span className=\"block truncate text-muted-foreground text-xs\">\n                              {item.description}\n                            </span>\n                          ) : null}\n                        </div>\n\n                        {selectedItem?.id === item.id && (\n                          <motion.span\n                            animate={shouldReduceMotion ? {} : { scale: 1 }}\n                            className=\"ml-2 shrink-0\"\n                            initial={shouldReduceMotion ? {} : { scale: 0 }}\n                            transition={\n                              shouldReduceMotion\n                                ? { duration: 0 }\n                                : {\n                                    damping: 25,\n                                    duration: 0.2,\n                                    mass: 0.5,\n                                    stiffness: 400,\n                                    type: \"spring\" as const,\n                                  }\n                            }\n                          >\n                            <svg\n                              className=\"h-4 w-4 text-brand\"\n                              fill=\"none\"\n                              stroke=\"currentColor\"\n                              viewBox=\"0 0 24 24\"\n                            >\n                              <title>Selected</title>\n                              <path\n                                d=\"M5 13l4 4L19 7\"\n                                strokeLinecap=\"round\"\n                                strokeLinejoin=\"round\"\n                                strokeWidth={2}\n                              />\n                            </svg>\n                          </motion.span>\n                        )}\n                      </button>\n                    </motion.li>\n                  ))\n                ) : (\n                  <motion.li\n                    animate={{ opacity: 1 }}\n                    className=\"px-4 py-8 text-center text-muted-foreground text-sm\"\n                    initial={\n                      shouldReduceMotion ? { opacity: 1 } : { opacity: 0 }\n                    }\n                    transition={\n                      shouldReduceMotion\n                        ? { duration: 0 }\n                        : {\n                            damping: 25,\n                            duration: 0.2,\n                            stiffness: 400,\n                            type: \"spring\" as const,\n                          }\n                    }\n                  >\n                    {emptyMessage}\n                  </motion.li>\n                )}\n              </AnimatePresence>\n            </ul>\n          </motion.div>\n        </div>\n      ) : null}\n    </AnimatePresence>\n  );\n\n  return (\n    <>\n      <div className={`relative inline-block ${className}`} ref={dropdownRef}>\n        <button\n          aria-expanded={isOpen}\n          aria-haspopup=\"listbox\"\n          aria-label={selectedItem ? `${label}: ${selectedItem.label}` : label}\n          className=\"flex min-h-[44px] w-full cursor-pointer items-center justify-between gap-2 rounded-lg border bg-background px-4 py-2 text-left transition-colors hover:bg-primary focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\"\n          id=\"dropdown-button\"\n          onClick={handleToggle}\n          ref={buttonRef}\n          type=\"button\"\n        >\n          <span className=\"block truncate\">\n            {String(selectedItem ? selectedItem.label : label)}\n          </span>\n          <motion.div\n            animate={{ rotate: isOpen ? ROTATION_ANGLE_OPEN : 0 }}\n            transition={\n              shouldReduceMotion\n                ? { duration: 0 }\n                : {\n                    damping: 25,\n                    duration: 0.2,\n                    stiffness: 400,\n                    type: \"spring\" as const,\n                  }\n            }\n          >\n            <ChevronDown className=\"h-4 w-4\" />\n          </motion.div>\n        </button>\n      </div>\n      {typeof window !== \"undefined\" &&\n        createPortal(dropdownContent, document.body)}\n    </>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/searchable-dropdown/index.tsx","type":"registry:ui"}],"name":"searchable-dropdown","registryDependencies":["https://smoothui.dev/r/tokens.json"],"title":"Searchable Dropdown","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["lucide-react","motion"],"description":"An animated Select dropdown component for SmoothUI wrapping Radix Select with smooth animations.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { Check, ChevronDown } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport {\n  DURATION_INSTANT,\n  SPRING_DEFAULT,\n  SPRING_SNAPPY,\n} from \"@/components/smoothui/lib/animation\";\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst CHEVRON_ROTATION = 180;\nconst DROPDOWN_OFFSET = 4;\nconst STAGGER_DELAY = 0.02;\nconst ITEM_HOVER_X = 2;\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface SelectOptionProps {\n  /** Whether the option is disabled */\n  disabled?: boolean;\n  /** The display label for the option */\n  label: string;\n  /** The value of the option */\n  value: string;\n}\n\nexport interface SelectGroupOption {\n  /** Label for the group */\n  label: string;\n  /** Options within this group */\n  options: SelectOptionProps[];\n}\n\nexport interface SelectProps {\n  /** Accessible label for the select */\n  \"aria-label\"?: string;\n  /** ID of element that labels this select */\n  \"aria-labelledby\"?: string;\n  /** Additional CSS class names for the trigger */\n  className?: string;\n  /** Additional CSS class names for the content dropdown */\n  contentClassName?: string;\n  /** The default value (uncontrolled) */\n  defaultValue?: string;\n  /** Whether the select is disabled */\n  disabled?: boolean;\n  /** Grouped options */\n  groups?: SelectGroupOption[];\n  /** The name attribute for form submission */\n  name?: string;\n  /** Callback when the value changes */\n  onValueChange?: (value: string) => void;\n  /** Flat list of options */\n  options?: SelectOptionProps[];\n  /** Placeholder text when no value is selected */\n  placeholder?: string;\n  /** Whether the select is required */\n  required?: boolean;\n  /** The size of the trigger */\n  size?: \"sm\" | \"default\";\n  /** The controlled value of the select */\n  value?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Component\n// ---------------------------------------------------------------------------\n\nexport default function Select({\n  value: controlledValue,\n  defaultValue,\n  onValueChange,\n  placeholder = \"Select an option\",\n  disabled = false,\n  required = false,\n  name,\n  options,\n  groups,\n  className,\n  contentClassName,\n  size = \"default\",\n  \"aria-label\": ariaLabel,\n  \"aria-labelledby\": ariaLabelledBy,\n}: SelectProps) {\n  const shouldReduceMotion = useReducedMotion();\n  const [isOpen, setIsOpen] = useState(false);\n  const [internalValue, setInternalValue] = useState(defaultValue ?? \"\");\n  const [focusedIndex, setFocusedIndex] = useState(-1);\n  const [position, setPosition] = useState({ left: 0, top: 0, width: 0 });\n\n  const triggerRef = useRef<HTMLButtonElement>(null);\n  const wrapperRef = useRef<HTMLDivElement>(null);\n  const portalRef = useRef<HTMLDivElement>(null);\n\n  const selectedValue =\n    controlledValue === undefined ? internalValue : controlledValue;\n\n  // Flatten all options for keyboard navigation\n  const allOptions: SelectOptionProps[] = (() => {\n    const flat: SelectOptionProps[] = [];\n    if (options) {\n      for (const opt of options) {\n        flat.push(opt);\n      }\n    }\n    if (groups) {\n      for (const group of groups) {\n        for (const opt of group.options) {\n          flat.push(opt);\n        }\n      }\n    }\n    return flat;\n  })();\n\n  const selectedLabel = allOptions.find(\n    (opt) => opt.value === selectedValue\n  )?.label;\n\n  // ---------------------------------------------------------------------------\n  // Handlers\n  // ---------------------------------------------------------------------------\n\n  const handleSelect = useCallback(\n    (opt: SelectOptionProps) => {\n      if (opt.disabled) {\n        return;\n      }\n      if (controlledValue === undefined) {\n        setInternalValue(opt.value);\n      }\n      onValueChange?.(opt.value);\n      setIsOpen(false);\n      setFocusedIndex(-1);\n      triggerRef.current?.focus();\n    },\n    [controlledValue, onValueChange]\n  );\n\n  const handleToggle = useCallback(() => {\n    if (disabled) {\n      return;\n    }\n    if (!isOpen && triggerRef.current) {\n      const rect = triggerRef.current.getBoundingClientRect();\n      setPosition({\n        left: rect.left,\n        top: rect.bottom + DROPDOWN_OFFSET,\n        width: rect.width,\n      });\n    }\n    setIsOpen((prev) => !prev);\n    setFocusedIndex(-1);\n  }, [disabled, isOpen]);\n\n  // ---------------------------------------------------------------------------\n  // Position updates on scroll/resize\n  // ---------------------------------------------------------------------------\n\n  useEffect(() => {\n    if (!(isOpen && triggerRef.current)) {\n      return;\n    }\n\n    const updatePosition = () => {\n      if (triggerRef.current) {\n        const rect = triggerRef.current.getBoundingClientRect();\n        setPosition({\n          left: rect.left,\n          top: rect.bottom + DROPDOWN_OFFSET,\n          width: rect.width,\n        });\n      }\n    };\n\n    window.addEventListener(\"scroll\", updatePosition, true);\n    window.addEventListener(\"resize\", updatePosition);\n    return () => {\n      window.removeEventListener(\"scroll\", updatePosition, true);\n      window.removeEventListener(\"resize\", updatePosition);\n    };\n  }, [isOpen]);\n\n  // ---------------------------------------------------------------------------\n  // Click outside to close\n  // ---------------------------------------------------------------------------\n\n  useEffect(() => {\n    if (!isOpen) {\n      return;\n    }\n\n    const handleClickOutside = (event: MouseEvent) => {\n      const target = event.target as Node;\n      if (\n        wrapperRef.current &&\n        !wrapperRef.current.contains(target) &&\n        portalRef.current &&\n        !portalRef.current.contains(target)\n      ) {\n        setIsOpen(false);\n        setFocusedIndex(-1);\n      }\n    };\n\n    document.addEventListener(\"mousedown\", handleClickOutside);\n    return () => document.removeEventListener(\"mousedown\", handleClickOutside);\n  }, [isOpen]);\n\n  // ---------------------------------------------------------------------------\n  // Keyboard navigation\n  // ---------------------------------------------------------------------------\n\n  useEffect(() => {\n    const handleKeyDown = (event: KeyboardEvent) => {\n      if (!isOpen) {\n        if (\n          (event.key === \"Enter\" || event.key === \" \") &&\n          document.activeElement === triggerRef.current\n        ) {\n          event.preventDefault();\n          handleToggle();\n        }\n        return;\n      }\n\n      if (event.key === \"Escape\") {\n        setIsOpen(false);\n        setFocusedIndex(-1);\n        triggerRef.current?.focus();\n      } else if (event.key === \"ArrowDown\") {\n        event.preventDefault();\n        setFocusedIndex((prev) =>\n          prev < allOptions.length - 1 ? prev + 1 : 0\n        );\n      } else if (event.key === \"ArrowUp\") {\n        event.preventDefault();\n        setFocusedIndex((prev) =>\n          prev > 0 ? prev - 1 : allOptions.length - 1\n        );\n      } else if (event.key === \"Enter\" && focusedIndex >= 0) {\n        event.preventDefault();\n        const opt = allOptions[focusedIndex];\n        if (opt) {\n          handleSelect(opt);\n        }\n      } else if (event.key === \"Home\") {\n        event.preventDefault();\n        setFocusedIndex(0);\n      } else if (event.key === \"End\") {\n        event.preventDefault();\n        setFocusedIndex(allOptions.length - 1);\n      }\n    };\n\n    document.addEventListener(\"keydown\", handleKeyDown);\n    return () => document.removeEventListener(\"keydown\", handleKeyDown);\n    // biome-ignore lint/correctness/useExhaustiveDependencies: handlers stable via closure\n  }, [isOpen, allOptions, focusedIndex, handleSelect, handleToggle]);\n\n  // ---------------------------------------------------------------------------\n  // Render helpers\n  // ---------------------------------------------------------------------------\n\n  /** Render a single option item with stagger animation */\n  const renderItem = (opt: SelectOptionProps, itemIndex: number) => {\n    const isSelected = opt.value === selectedValue;\n    const isFocused = itemIndex === focusedIndex;\n\n    return (\n      <motion.div\n        animate={shouldReduceMotion ? { opacity: 1 } : { opacity: 1, x: 0 }}\n        exit={\n          shouldReduceMotion\n            ? { opacity: 0, transition: { duration: 0 } }\n            : { opacity: 0, x: -8 }\n        }\n        initial={shouldReduceMotion ? { opacity: 1 } : { opacity: 0, x: -8 }}\n        key={opt.value}\n        transition={\n          shouldReduceMotion\n            ? DURATION_INSTANT\n            : {\n                ...SPRING_SNAPPY,\n                delay: itemIndex * STAGGER_DELAY,\n              }\n        }\n        whileHover={shouldReduceMotion ? {} : { x: ITEM_HOVER_X }}\n      >\n        <button\n          aria-selected={isSelected}\n          className={cn(\n            \"relative flex w-full cursor-default select-none items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-left text-sm outline-hidden\",\n            \"transition-colors\",\n            opt.disabled\n              ? \"pointer-events-none opacity-50\"\n              : \"hover:bg-accent hover:text-white\",\n            isFocused && \"bg-accent text-white\",\n            isSelected && \"font-medium\"\n          )}\n          disabled={opt.disabled}\n          onClick={() => handleSelect(opt)}\n          onMouseEnter={() => setFocusedIndex(itemIndex)}\n          role=\"option\"\n          type=\"button\"\n        >\n          <span className=\"flex-1 truncate\">{opt.label}</span>\n\n          {/* Animated checkmark */}\n          <span className=\"absolute right-2 flex size-3.5 items-center justify-center\">\n            <AnimatePresence>\n              {isSelected && (\n                <motion.span\n                  animate={shouldReduceMotion ? {} : { opacity: 1, scale: 1 }}\n                  exit={\n                    shouldReduceMotion\n                      ? { opacity: 0, transition: { duration: 0 } }\n                      : { opacity: 0, scale: 0 }\n                  }\n                  initial={shouldReduceMotion ? {} : { opacity: 0, scale: 0 }}\n                  transition={\n                    shouldReduceMotion\n                      ? DURATION_INSTANT\n                      : {\n                          damping: 20,\n                          duration: 0.2,\n                          stiffness: 300,\n                          type: \"spring\" as const,\n                        }\n                  }\n                >\n                  <Check className=\"size-4\" />\n                </motion.span>\n              )}\n            </AnimatePresence>\n          </span>\n        </button>\n      </motion.div>\n    );\n  };\n\n  // Global index counter for stagger across groups\n  let globalIndex = 0;\n\n  // ---------------------------------------------------------------------------\n  // Dropdown content (portalled)\n  // ---------------------------------------------------------------------------\n\n  const dropdownContent = (\n    <AnimatePresence>\n      {isOpen ? (\n        <div ref={portalRef}>\n          <motion.div\n            animate={\n              shouldReduceMotion\n                ? { opacity: 1 }\n                : { opacity: 1, scale: 1, y: 0 }\n            }\n            className={cn(\n              \"fixed z-50 origin-top overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md\",\n              contentClassName\n            )}\n            exit={\n              shouldReduceMotion\n                ? { opacity: 0, transition: { duration: 0 } }\n                : {\n                    opacity: 0,\n                    scale: 0.95,\n                    transition: { duration: 0.15 },\n                    y: -4,\n                  }\n            }\n            initial={\n              shouldReduceMotion\n                ? { opacity: 1 }\n                : { opacity: 0, scale: 0.95, y: -4 }\n            }\n            role=\"listbox\"\n            style={{\n              left: `${position.left}px`,\n              top: `${position.top}px`,\n              width: `${position.width}px`,\n            }}\n            transition={shouldReduceMotion ? DURATION_INSTANT : SPRING_DEFAULT}\n          >\n            <div className=\"max-h-60 overflow-y-auto p-1\">\n              {/* Flat options */}\n              {options &&\n                options.length > 0 &&\n                (() => {\n                  const items = options.map((opt) => {\n                    const idx = globalIndex;\n                    globalIndex += 1;\n                    return renderItem(opt, idx);\n                  });\n                  return items;\n                })()}\n\n              {/* Grouped options */}\n              {groups\n                ? groups.map((group, groupIdx) => {\n                    const groupItems = group.options.map((opt) => {\n                      const idx = globalIndex;\n                      globalIndex += 1;\n                      return renderItem(opt, idx);\n                    });\n\n                    return (\n                      <div key={group.label}>\n                        {groupIdx > 0 && (\n                          <div className=\"pointer-events-none -mx-1 my-1 h-px bg-border\" />\n                        )}\n                        <div className=\"px-2 py-1.5 text-muted-foreground text-xs\">\n                          {group.label}\n                        </div>\n                        {groupItems}\n                      </div>\n                    );\n                  })\n                : null}\n            </div>\n          </motion.div>\n        </div>\n      ) : null}\n    </AnimatePresence>\n  );\n\n  // ---------------------------------------------------------------------------\n  // Main render\n  // ---------------------------------------------------------------------------\n\n  return (\n    <>\n      <div className=\"relative inline-block w-full\" ref={wrapperRef}>\n        {/* Hidden native input for form submission */}\n        {name ? (\n          <input\n            aria-hidden=\"true\"\n            name={name}\n            required={required}\n            tabIndex={-1}\n            type=\"hidden\"\n            value={selectedValue}\n          />\n        ) : null}\n\n        <button\n          aria-expanded={isOpen}\n          aria-haspopup=\"listbox\"\n          aria-label={ariaLabel}\n          aria-labelledby={ariaLabelledBy}\n          aria-required={required || undefined}\n          className={cn(\n            \"flex w-full cursor-pointer items-center justify-between gap-2 whitespace-nowrap rounded-md border border-input bg-background px-3 py-2 text-sm shadow-xs outline-none transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground\",\n            size === \"default\" ? \"h-9\" : \"h-8\",\n            className\n          )}\n          data-placeholder={!selectedLabel || undefined}\n          disabled={disabled}\n          onClick={handleToggle}\n          ref={triggerRef}\n          role=\"combobox\"\n          type=\"button\"\n        >\n          <span\n            className={cn(\n              \"line-clamp-1 flex items-center gap-2 text-left\",\n              !selectedLabel && \"text-muted-foreground\"\n            )}\n          >\n            {selectedLabel ?? placeholder}\n          </span>\n\n          {/* Animated chevron */}\n          <motion.div\n            animate={{ rotate: isOpen ? CHEVRON_ROTATION : 0 }}\n            className=\"shrink-0\"\n            transition={\n              shouldReduceMotion\n                ? DURATION_INSTANT\n                : { bounce: 0.05, duration: 0.25, type: \"spring\" as const }\n            }\n          >\n            <ChevronDown className=\"size-4 opacity-50\" />\n          </motion.div>\n        </button>\n      </div>\n\n      {typeof window === \"undefined\"\n        ? null\n        : createPortal(dropdownContent, document.body)}\n    </>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/select/index.tsx","type":"registry:ui"}],"name":"select","registryDependencies":["https://smoothui.dev/r/lib.json"],"title":"Select","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":[],"description":"Demo 3 adaptation: noisy circular reveal with radial interpolation.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport ShaderRevealTransition from \"../shader-reveal-transition\";\n\nexport interface ShaderRevealCircleTransitionProps {\n  children: ReactNode;\n  className?: string;\n  duration?: number;\n  onRest?: () => void;\n  transitionKey: string | number;\n}\n\nconst ShaderRevealCircleTransition = ({\n  children,\n  className,\n  duration = 1080,\n  onRest,\n  transitionKey,\n}: ShaderRevealCircleTransitionProps) => (\n  <ShaderRevealTransition\n    className={className}\n    duration={duration}\n    onRest={onRest}\n    transitionKey={transitionKey}\n    variant=\"circle\"\n  >\n    {children}\n  </ShaderRevealTransition>\n);\n\nexport default ShaderRevealCircleTransition;\n","path":"index.tsx","target":"components/smoothui/shader-reveal-circle-transition/index.tsx","type":"registry:ui"}],"name":"shader-reveal-circle-transition","registryDependencies":["https://smoothui.dev/r/shader-reveal-transition.json"],"title":"Shader Reveal Circle Transition","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":[],"description":"Demo 5 adaptation: luminance-style vertical displacement translated into a shader overlay.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport ShaderRevealTransition from \"../shader-reveal-transition\";\n\nexport interface ShaderRevealLumaTransitionProps {\n  children: ReactNode;\n  className?: string;\n  duration?: number;\n  onRest?: () => void;\n  transitionKey: string | number;\n}\n\nconst ShaderRevealLumaTransition = ({\n  children,\n  className,\n  duration = 1080,\n  onRest,\n  transitionKey,\n}: ShaderRevealLumaTransitionProps) => (\n  <ShaderRevealTransition\n    className={className}\n    duration={duration}\n    onRest={onRest}\n    transitionKey={transitionKey}\n    variant=\"luma\"\n  >\n    {children}\n  </ShaderRevealTransition>\n);\n\nexport default ShaderRevealLumaTransition;\n","path":"index.tsx","target":"components/smoothui/shader-reveal-luma-transition/index.tsx","type":"registry:ui"}],"name":"shader-reveal-luma-transition","registryDependencies":["https://smoothui.dev/r/shader-reveal-transition.json"],"title":"Shader Reveal Luma Transition","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":[],"description":"Demo 1 adaptation: 4D-noise threshold transition with an organic shader gate.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport ShaderRevealTransition from \"../shader-reveal-transition\";\n\nexport interface ShaderRevealNoiseTransitionProps {\n  children: ReactNode;\n  className?: string;\n  duration?: number;\n  onRest?: () => void;\n  transitionKey: string | number;\n}\n\nconst ShaderRevealNoiseTransition = ({\n  children,\n  className,\n  duration = 1080,\n  onRest,\n  transitionKey,\n}: ShaderRevealNoiseTransitionProps) => (\n  <ShaderRevealTransition\n    className={className}\n    duration={duration}\n    onRest={onRest}\n    transitionKey={transitionKey}\n    variant=\"noise\"\n  >\n    {children}\n  </ShaderRevealTransition>\n);\n\nexport default ShaderRevealNoiseTransition;\n","path":"index.tsx","target":"components/smoothui/shader-reveal-noise-transition/index.tsx","type":"registry:ui"}],"name":"shader-reveal-noise-transition","registryDependencies":["https://smoothui.dev/r/shader-reveal-transition.json"],"title":"Shader Reveal Noise Transition","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":[],"description":"Demo 6 adaptation: rotated displacement vectors with a planetary swirl feel.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport ShaderRevealTransition from \"../shader-reveal-transition\";\n\nexport interface ShaderRevealPlanetaryTransitionProps {\n  children: ReactNode;\n  className?: string;\n  duration?: number;\n  onRest?: () => void;\n  transitionKey: string | number;\n}\n\nconst ShaderRevealPlanetaryTransition = ({\n  children,\n  className,\n  duration = 1080,\n  onRest,\n  transitionKey,\n}: ShaderRevealPlanetaryTransitionProps) => (\n  <ShaderRevealTransition\n    className={className}\n    duration={duration}\n    onRest={onRest}\n    transitionKey={transitionKey}\n    variant=\"planetary\"\n  >\n    {children}\n  </ShaderRevealTransition>\n);\n\nexport default ShaderRevealPlanetaryTransition;\n","path":"index.tsx","target":"components/smoothui/shader-reveal-planetary-transition/index.tsx","type":"registry:ui"}],"name":"shader-reveal-planetary-transition","registryDependencies":["https://smoothui.dev/r/shader-reveal-transition.json"],"title":"Shader Reveal Planetary Transition","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":[],"description":"Demo 8 adaptation: procedural noise push/pull displacement.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport ShaderRevealTransition from \"../shader-reveal-transition\";\n\nexport interface ShaderRevealPushTransitionProps {\n  children: ReactNode;\n  className?: string;\n  duration?: number;\n  onRest?: () => void;\n  transitionKey: string | number;\n}\n\nconst ShaderRevealPushTransition = ({\n  children,\n  className,\n  duration = 1080,\n  onRest,\n  transitionKey,\n}: ShaderRevealPushTransitionProps) => (\n  <ShaderRevealTransition\n    className={className}\n    duration={duration}\n    onRest={onRest}\n    transitionKey={transitionKey}\n    variant=\"push\"\n  >\n    {children}\n  </ShaderRevealTransition>\n);\n\nexport default ShaderRevealPushTransition;\n","path":"index.tsx","target":"components/smoothui/shader-reveal-push-transition/index.tsx","type":"registry:ui"}],"name":"shader-reveal-push-transition","registryDependencies":["https://smoothui.dev/r/shader-reveal-transition.json"],"title":"Shader Reveal Push Transition","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":[],"description":"Demo 7 adaptation: divided UV stripe displacement with diagonal motion.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport ShaderRevealTransition from \"../shader-reveal-transition\";\n\nexport interface ShaderRevealStripesTransitionProps {\n  children: ReactNode;\n  className?: string;\n  duration?: number;\n  onRest?: () => void;\n  transitionKey: string | number;\n}\n\nconst ShaderRevealStripesTransition = ({\n  children,\n  className,\n  duration = 1080,\n  onRest,\n  transitionKey,\n}: ShaderRevealStripesTransitionProps) => (\n  <ShaderRevealTransition\n    className={className}\n    duration={duration}\n    onRest={onRest}\n    transitionKey={transitionKey}\n    variant=\"stripes\"\n  >\n    {children}\n  </ShaderRevealTransition>\n);\n\nexport default ShaderRevealStripesTransition;\n","path":"index.tsx","target":"components/smoothui/shader-reveal-stripes-transition/index.tsx","type":"registry:ui"}],"name":"shader-reveal-stripes-transition","registryDependencies":["https://smoothui.dev/r/shader-reveal-transition.json"],"title":"Shader Reveal Stripes Transition","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"A shared WebGL transition engine with eight shader-driven reveal variants.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { useReducedMotion } from \"motion/react\";\nimport { type ReactNode, useEffect, useRef, useState } from \"react\";\n\nexport type ShaderRevealVariant =\n  | \"noise\"\n  | \"zoom\"\n  | \"circle\"\n  | \"wipe\"\n  | \"luma\"\n  | \"planetary\"\n  | \"stripes\"\n  | \"push\";\n\nexport interface ShaderRevealTransitionProps {\n  children: ReactNode;\n  className?: string;\n  duration?: number;\n  onRest?: () => void;\n  transitionKey: string | number;\n  variant?: ShaderRevealVariant;\n}\n\nconst DEFAULT_DURATION_MS = 1080;\nconst MIDPOINT = 0.5;\nconst MAX_DPR = 2;\nconst VERTEX_SHADER =\n  \"attribute vec2 a; void main(){ gl_Position = vec4(a, 0.0, 1.0); }\";\nconst FRAGMENT_SHADER = `\nprecision mediump float;\nuniform vec2 uRes;\nuniform float uTime;\nuniform float uProgress;\nuniform float uAlpha;\nuniform float uVariant;\n#define PI 3.14159265359\n\nfloat hash(vec2 p){ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123); }\nfloat noise(vec2 p){\n  vec2 i = floor(p);\n  vec2 f = fract(p);\n  float a = hash(i);\n  float b = hash(i + vec2(1.0, 0.0));\n  float c = hash(i + vec2(0.0, 1.0));\n  float d = hash(i + vec2(1.0, 1.0));\n  vec2 u = f * f * (3.0 - 2.0 * f);\n  return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y;\n}\nfloat parabola(float x, float k){ return pow(4.0 * x * (1.0 - x), k); }\nmat2 rot(float a){ float s = sin(a); float c = cos(a); return mat2(c, -s, s, c); }\n\nvec4 paint(vec2 uv, float mask, vec3 color, float glow){\n  float exitFade = smoothstep(1.0, 0.76, uProgress);\n  float entryFade = smoothstep(0.0, 0.10, uProgress);\n  float alpha = clamp((mask * 0.76 + glow * 0.24) * entryFade * exitFade * uAlpha, 0.0, 0.92);\n  return vec4(color * alpha + vec3(1.0) * glow * 0.16 * uAlpha, alpha);\n}\n\nvoid main(){\n  vec2 uv = gl_FragCoord.xy / uRes;\n  vec2 aspect = vec2(uRes.x / uRes.y, 1.0);\n  vec2 p = (uv - 0.5) * aspect;\n  float progress = smoothstep(0.0, 1.0, uProgress);\n  vec3 pink = vec3(1.0, 0.20, 0.58);\n  vec3 cyan = vec3(0.12, 0.86, 1.0);\n  vec3 gold = vec3(1.0, 0.72, 0.28);\n  vec3 mint = vec3(0.45, 1.0, 0.72);\n\n  float mask = 0.0;\n  float glow = 0.0;\n  vec3 color = mix(pink, cyan, uv.x);\n\n  if (uVariant < 0.5) {\n    // Demo 1 — turbulent noise threshold. Chunky, organic gate.\n    float n1 = noise(uv * vec2(18.0, 12.0) + uTime * 0.42);\n    float n2 = noise(uv * vec2(52.0, 44.0) - uTime * 0.18);\n    float threshold = progress * 1.35 - 0.18;\n    float gate = smoothstep(threshold - 0.18, threshold + 0.18, uv.x * 0.62 + uv.y * 0.28 + n1 * 0.34 + n2 * 0.08);\n    mask = pow(gate * (1.0 - gate) * 4.0, 0.74) + step(0.74, n2) * gate * 0.22;\n    glow = exp(-pow(uv.x + n1 * 0.26 - progress, 2.0) * 26.0);\n    color = mix(vec3(1.0, 0.10, 0.55), vec3(0.18, 1.0, 0.78), n1);\n  } else if (uVariant < 1.5) {\n    // Demo 2 — zoom mix. Large central lens, not a sweep.\n    float yGate = smoothstep(0.0, 1.0, progress * 2.15 + uv.y - 1.08);\n    vec2 z = (uv - 0.5) * mix(1.75, 0.35, yGate) + 0.5;\n    float lens = 1.0 - smoothstep(0.12, 0.78, length((z - 0.5) * aspect));\n    float seam = exp(-pow(yGate - 0.5, 2.0) * 20.0);\n    mask = (lens * 0.72 + seam * 0.42) * parabola(progress, 0.42);\n    glow = seam * 0.9 + lens * 0.25;\n    color = mix(vec3(1.0, 0.80, 0.34), vec3(0.70, 0.92, 1.0), yGate);\n  } else if (uVariant < 2.5) {\n    // Demo 3 — noisy circular reveal.\n    float n = noise(uv * 9.0 + uTime * 0.25);\n    float radius = progress * 1.08 + n * 0.045;\n    float d = length(p) - radius * 0.72;\n    mask = exp(-d * d * 42.0) + smoothstep(0.08, -0.12, d) * 0.18;\n    glow = exp(-d * d * 140.0);\n    color = mix(vec3(1.0, 0.22, 0.58), vec3(1.0, 0.75, 0.32), atan(p.y, p.x) / PI * 0.5 + 0.5);\n  } else if (uVariant < 3.5) {\n    // Demo 4 — displacement wipe. Narrow noisy blade.\n    float n = noise(uv * vec2(12.0, 26.0) + vec2(uTime * 0.26, -uTime * 0.12));\n    float blade = progress * 1.32 - 0.16 + n * 0.11;\n    float edge = blade - uv.x;\n    float front = exp(-edge * edge * 120.0);\n    float fillSide = smoothstep(-0.03, 0.16, edge) * 0.18;\n    mask = front * 0.92 + fillSide;\n    glow = front;\n    color = mix(vec3(0.12, 0.92, 1.0), vec3(1.0, 1.0, 1.0), front * 0.38);\n  } else if (uVariant < 4.5) {\n    // Demo 5 — luminance displacement. Horizontal ribbons pulled vertically.\n    float lum = noise(uv * 5.0 + vec2(0.0, uTime * 0.22));\n    float displacedY = uv.y + progress * (lum - 0.5) * 0.55;\n    float ribbons = sin(displacedY * 58.0 + uTime * 1.25) * 0.5 + 0.5;\n    float fine = sin(displacedY * 132.0 - uTime * 1.8) * 0.5 + 0.5;\n    float window = smoothstep(0.03, 0.5, progress) * smoothstep(1.0, 0.72, progress);\n    mask = (smoothstep(0.58, 0.92, ribbons) * 0.74 + smoothstep(0.76, 0.98, fine) * 0.22) * window;\n    glow = smoothstep(0.84, 1.0, ribbons) * window;\n    color = mix(vec3(1.0, 0.68, 0.22), vec3(1.0, 0.20, 0.62), lum);\n  } else if (uVariant < 5.5) {\n    // Demo 6 — planetary rotated displacement. Spiral vortex.\n    vec2 disp = vec2(noise(uv * 5.5 + uTime * 0.12), noise(uv * 5.5 + 4.2 - uTime * 0.10)) - 0.5;\n    vec2 q = rot(PI * 0.25) * (p + disp * 0.44 * parabola(progress, 0.55));\n    float ang = atan(q.y, q.x);\n    float rr = length(q);\n    float arms = sin(ang * 5.0 + rr * 18.0 - uTime * 1.8);\n    float core = 1.0 - smoothstep(0.0, 0.42, rr);\n    mask = (smoothstep(0.08, 0.92, arms) * 0.72 + core * 0.38) * parabola(progress, 0.58);\n    glow = exp(-abs(arms) * 1.5) * parabola(progress, 0.85);\n    color = mix(vec3(0.10, 0.88, 1.0), vec3(1.0, 0.66, 0.18), rr + core * 0.35);\n  } else if (uVariant < 6.5) {\n    // Demo 7 — divided UV stripe displacement. Barcode shutter.\n    vec2 divided = fract(uv * vec2(42.0, 1.0));\n    float stripeCore = step(0.18, divided.x) * step(divided.x, 0.62);\n    float diagonal = uv.x + uv.y * 0.18 + divided.x * 0.06;\n    float sweep = smoothstep(-0.10, 1.12, progress * 1.32 - diagonal + 0.18);\n    float shutter = stripeCore * sweep * smoothstep(0.04, 0.56, progress) * smoothstep(1.0, 0.78, progress);\n    mask = shutter * 0.92;\n    glow = stripeCore * exp(-pow(progress - uv.x, 2.0) * 30.0) * 0.86;\n    color = mix(vec3(1.0, 0.15, 0.55), vec3(0.12, 0.88, 1.0), divided.x);\n  } else {\n    // Demo 8 — noise push/pull. Vertical flow with directional pressure.\n    float hn = noise(uv * uRes / 115.0 + uTime * 0.12);\n    vec2 centerDir = normalize(vec2(0.5) - uv + 0.001);\n    float pull = centerDir.y * (1.0 + hn * 0.7) * parabola(progress, 0.62);\n    float bands = sin((uv.y + pull * 0.52) * 28.0 + hn * 5.0);\n    float pressure = smoothstep(-0.22, 0.96, bands) * parabola(progress, 0.62);\n    float verticalGlow = exp(-pow(uv.y - (0.5 + pull * 0.22), 2.0) * 18.0);\n    mask = pressure * 0.78 + verticalGlow * 0.22;\n    glow = verticalGlow * pressure;\n    color = mix(vec3(0.48, 1.0, 0.74), vec3(1.0, 0.22, 0.58), hn);\n  }\n\n  gl_FragColor = paint(uv, mask, color, glow);\n}\n`;\nconst STRIP = new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]);\n\ninterface Playback {\n  cancel: () => void;\n}\ninterface Controller {\n  destroy: () => void;\n  setAlpha: (alpha: number) => void;\n  setProgress: (progress: number) => void;\n  setVariant: (variant: ShaderRevealVariant) => void;\n}\n\nconst clamp01 = (value: number) => Math.max(0, Math.min(1, value));\nconst easeOutQuart = (value: number) => 1 - (1 - value) ** 4;\nconst easeInOutCubic = (value: number) =>\n  value < 0.5 ? 4 * value ** 3 : 1 - (-2 * value + 2) ** 3 / 2;\n\nconst variantToUniform = (variant: ShaderRevealVariant) => {\n  switch (variant) {\n    case \"noise\":\n      return 0;\n    case \"zoom\":\n      return 1;\n    case \"circle\":\n      return 2;\n    case \"wipe\":\n      return 3;\n    case \"luma\":\n      return 4;\n    case \"planetary\":\n      return 5;\n    case \"stripes\":\n      return 6;\n    default:\n      return 7;\n  }\n};\n\nconst compile = (gl: WebGLRenderingContext, type: number, source: string) => {\n  const shader = gl.createShader(type);\n  if (!shader) {\n    return null;\n  }\n  gl.shaderSource(shader, source);\n  gl.compileShader(shader);\n  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n    gl.deleteShader(shader);\n    return null;\n  }\n  return shader;\n};\n\nconst resize = (canvas: HTMLCanvasElement, gl: WebGLRenderingContext) => {\n  const rect = canvas.getBoundingClientRect();\n  const dpr = Math.min(window.devicePixelRatio || 1, MAX_DPR);\n  const width = Math.max(1, Math.round(rect.width * dpr));\n  const height = Math.max(1, Math.round(rect.height * dpr));\n  if (canvas.width !== width || canvas.height !== height) {\n    canvas.width = width;\n    canvas.height = height;\n  }\n  gl.viewport(0, 0, width, height);\n};\n\nconst createController = (\n  canvas: HTMLCanvasElement,\n  initialVariant: ShaderRevealVariant\n): Controller | null => {\n  const gl = canvas.getContext(\"webgl\", {\n    alpha: true,\n    antialias: false,\n    premultipliedAlpha: true,\n  });\n  if (!gl) {\n    return null;\n  }\n  const vertex = compile(gl, gl.VERTEX_SHADER, VERTEX_SHADER);\n  const fragment = compile(gl, gl.FRAGMENT_SHADER, FRAGMENT_SHADER);\n  const program = gl.createProgram();\n  if (!(vertex && fragment && program)) {\n    return null;\n  }\n  gl.attachShader(program, vertex);\n  gl.attachShader(program, fragment);\n  gl.linkProgram(program);\n  gl.deleteShader(vertex);\n  gl.deleteShader(fragment);\n  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n    gl.deleteProgram(program);\n    return null;\n  }\n\n  // biome-ignore lint/correctness/useHookAtTopLevel: WebGLRenderingContext.useProgram is not a React hook.\n  gl.useProgram(program);\n  const buffer = gl.createBuffer();\n  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);\n  gl.bufferData(gl.ARRAY_BUFFER, STRIP, gl.STATIC_DRAW);\n  const position = gl.getAttribLocation(program, \"a\");\n  gl.enableVertexAttribArray(position);\n  gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0);\n  gl.enable(gl.BLEND);\n  gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);\n\n  const uRes = gl.getUniformLocation(program, \"uRes\");\n  const uTime = gl.getUniformLocation(program, \"uTime\");\n  const uProgress = gl.getUniformLocation(program, \"uProgress\");\n  const uAlpha = gl.getUniformLocation(program, \"uAlpha\");\n  const uVariant = gl.getUniformLocation(program, \"uVariant\");\n  const startedAt = performance.now();\n  let progress = 0;\n  let alpha = 0;\n  let variant = initialVariant;\n  let frame = 0;\n  let destroyed = false;\n\n  const tick = () => {\n    if (destroyed) {\n      return;\n    }\n    resize(canvas, gl);\n    gl.clearColor(0, 0, 0, 0);\n    gl.clear(gl.COLOR_BUFFER_BIT);\n    gl.uniform2f(uRes, canvas.width, canvas.height);\n    gl.uniform1f(uTime, (performance.now() - startedAt) / 1000);\n    gl.uniform1f(uProgress, progress);\n    gl.uniform1f(uAlpha, alpha);\n    gl.uniform1f(uVariant, variantToUniform(variant));\n    gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);\n    frame = requestAnimationFrame(tick);\n  };\n  frame = requestAnimationFrame(tick);\n\n  return {\n    destroy: () => {\n      destroyed = true;\n      cancelAnimationFrame(frame);\n      gl.clear(gl.COLOR_BUFFER_BIT);\n      if (buffer) {\n        gl.deleteBuffer(buffer);\n      }\n      gl.deleteProgram(program);\n    },\n    setAlpha: (nextAlpha: number) => {\n      alpha = clamp01(nextAlpha);\n    },\n    setProgress: (nextProgress: number) => {\n      progress = clamp01(nextProgress);\n    },\n    setVariant: (nextVariant: ShaderRevealVariant) => {\n      variant = nextVariant;\n    },\n  };\n};\n\nconst play = (\n  controller: Controller,\n  duration: number,\n  onMidpoint: () => void,\n  onComplete: () => void\n): Playback => {\n  let frame = 0;\n  let cancelled = false;\n  let didSwap = false;\n  const startedAt = performance.now();\n  controller.setAlpha(1);\n\n  const advance = () => {\n    if (cancelled) {\n      return;\n    }\n    const raw = clamp01((performance.now() - startedAt) / duration);\n    const progress = easeOutQuart(raw);\n    controller.setProgress(progress);\n    if (!(didSwap || progress < MIDPOINT)) {\n      didSwap = true;\n      onMidpoint();\n    }\n    if (raw < 1) {\n      frame = requestAnimationFrame(advance);\n      return;\n    }\n    if (!didSwap) {\n      onMidpoint();\n    }\n    const fadeStartedAt = performance.now();\n    const fade = () => {\n      if (cancelled) {\n        return;\n      }\n      const fadeRaw = clamp01((performance.now() - fadeStartedAt) / 90);\n      controller.setAlpha(1 - easeInOutCubic(fadeRaw));\n      if (fadeRaw < 1) {\n        frame = requestAnimationFrame(fade);\n        return;\n      }\n      controller.setAlpha(0);\n      controller.setProgress(0);\n      onComplete();\n    };\n    frame = requestAnimationFrame(fade);\n  };\n  frame = requestAnimationFrame(advance);\n\n  return {\n    cancel: () => {\n      cancelled = true;\n      cancelAnimationFrame(frame);\n    },\n  };\n};\n\nconst ShaderRevealTransition = ({\n  children,\n  className,\n  duration = DEFAULT_DURATION_MS,\n  onRest,\n  transitionKey,\n  variant = \"noise\",\n}: ShaderRevealTransitionProps) => {\n  const shouldReduceMotion = useReducedMotion();\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const controllerRef = useRef<Controller | null>(null);\n  const playbackRef = useRef<Playback | null>(null);\n  const previousKey = useRef(transitionKey);\n  const [renderedChildren, setRenderedChildren] = useState(children);\n  const [isActive, setIsActive] = useState(false);\n\n  useEffect(() => {\n    const canvas = canvasRef.current;\n    if (!canvas) {\n      return;\n    }\n    const controller = createController(canvas, variant);\n    controllerRef.current = controller;\n    return () => {\n      playbackRef.current?.cancel();\n      controller?.destroy();\n      controllerRef.current = null;\n    };\n  }, [variant]);\n\n  useEffect(() => {\n    if (previousKey.current === transitionKey) {\n      setRenderedChildren(children);\n      return;\n    }\n    previousKey.current = transitionKey;\n    playbackRef.current?.cancel();\n\n    if (shouldReduceMotion) {\n      setRenderedChildren(children);\n      setIsActive(false);\n      onRest?.();\n      return;\n    }\n\n    const controller = controllerRef.current;\n    if (!controller) {\n      setRenderedChildren(children);\n      onRest?.();\n      return;\n    }\n    controller.setVariant(variant);\n    setIsActive(true);\n    playbackRef.current = play(\n      controller,\n      duration,\n      () => setRenderedChildren(children),\n      () => {\n        setIsActive(false);\n        playbackRef.current = null;\n        onRest?.();\n      }\n    );\n  }, [children, duration, onRest, shouldReduceMotion, transitionKey, variant]);\n\n  return (\n    <div\n      className={cn(\n        \"relative isolate overflow-hidden rounded-2xl bg-background text-foreground\",\n        className\n      )}\n      data-transitioning={isActive ? \"true\" : \"false\"}\n    >\n      <div className=\"relative z-0\">{renderedChildren}</div>\n      <canvas\n        className={cn(\n          \"pointer-events-none absolute inset-0 z-10 h-full w-full transition-opacity duration-75\",\n          isActive && !shouldReduceMotion ? \"opacity-100\" : \"opacity-0\"\n        )}\n        ref={canvasRef}\n      />\n    </div>\n  );\n};\n\nexport default ShaderRevealTransition;\n","path":"index.tsx","target":"components/smoothui/shader-reveal-transition/index.tsx","type":"registry:ui"}],"name":"shader-reveal-transition","registryDependencies":[],"title":"Shader Reveal Transition","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":[],"description":"Demo 4 adaptation: displacement-noise horizontal wipe with a sharp eased edge.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport ShaderRevealTransition from \"../shader-reveal-transition\";\n\nexport interface ShaderRevealWipeTransitionProps {\n  children: ReactNode;\n  className?: string;\n  duration?: number;\n  onRest?: () => void;\n  transitionKey: string | number;\n}\n\nconst ShaderRevealWipeTransition = ({\n  children,\n  className,\n  duration = 1080,\n  onRest,\n  transitionKey,\n}: ShaderRevealWipeTransitionProps) => (\n  <ShaderRevealTransition\n    className={className}\n    duration={duration}\n    onRest={onRest}\n    transitionKey={transitionKey}\n    variant=\"wipe\"\n  >\n    {children}\n  </ShaderRevealTransition>\n);\n\nexport default ShaderRevealWipeTransition;\n","path":"index.tsx","target":"components/smoothui/shader-reveal-wipe-transition/index.tsx","type":"registry:ui"}],"name":"shader-reveal-wipe-transition","registryDependencies":["https://smoothui.dev/r/shader-reveal-transition.json"],"title":"Shader Reveal Wipe Transition","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":[],"description":"Demo 2 adaptation: vertical-progress zoom mix translated into a UI frame transition.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport ShaderRevealTransition from \"../shader-reveal-transition\";\n\nexport interface ShaderRevealZoomTransitionProps {\n  children: ReactNode;\n  className?: string;\n  duration?: number;\n  onRest?: () => void;\n  transitionKey: string | number;\n}\n\nconst ShaderRevealZoomTransition = ({\n  children,\n  className,\n  duration = 1080,\n  onRest,\n  transitionKey,\n}: ShaderRevealZoomTransitionProps) => (\n  <ShaderRevealTransition\n    className={className}\n    duration={duration}\n    onRest={onRest}\n    transitionKey={transitionKey}\n    variant=\"zoom\"\n  >\n    {children}\n  </ShaderRevealTransition>\n);\n\nexport default ShaderRevealZoomTransition;\n","path":"index.tsx","target":"components/smoothui/shader-reveal-zoom-transition/index.tsx","type":"registry:ui"}],"name":"shader-reveal-zoom-transition","registryDependencies":["https://smoothui.dev/r/shader-reveal-transition.json"],"title":"Shader Reveal Zoom Transition","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"A SharedAxisX text animation component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useEffect, useState } from \"react\";\n\nexport interface SharedAxisXProps {\n  className?: string;\n  /** Interval between phrase swaps in milliseconds. */\n  interval?: number;\n  phrases: string[];\n}\n\nconst ENTER_EASE = [0.2, 0, 0, 1] as const;\nconst EXIT_EASE = [0.4, 0, 1, 1] as const;\n\n/**\n * SharedAxisX — horizontal shared-axis phrase transition inspired by Google Material.\n * Outgoing phrase slides left as the next slides in from the right.\n * From the animate-text catalog (`shared-axis-x`).\n */\nexport default function SharedAxisX({\n  phrases,\n  className = \"\",\n  interval = 2500,\n}: SharedAxisXProps) {\n  const [index, setIndex] = useState(0);\n  const shouldReduceMotion = useReducedMotion();\n\n  useEffect(() => {\n    const id = setInterval(() => {\n      setIndex((prev) => (prev + 1) % phrases.length);\n    }, interval);\n    return () => clearInterval(id);\n  }, [interval, phrases.length]);\n\n  return (\n    <span\n      aria-live=\"polite\"\n      className={`relative inline-block overflow-hidden ${className}`}\n    >\n      <AnimatePresence mode=\"wait\">\n        <motion.span\n          animate={\n            shouldReduceMotion ? { opacity: 1 } : { opacity: 1, scale: 1, x: 0 }\n          }\n          exit={\n            shouldReduceMotion\n              ? { opacity: 0, transition: { duration: 0 } }\n              : {\n                  opacity: 0,\n                  scale: 0.98,\n                  transition: { duration: 0.36, ease: EXIT_EASE },\n                  x: -20,\n                }\n          }\n          initial={\n            shouldReduceMotion\n              ? { opacity: 1 }\n              : { opacity: 0, scale: 0.98, x: 24 }\n          }\n          key={index}\n          style={{ display: \"inline-block\" }}\n          transition={\n            shouldReduceMotion\n              ? { duration: 0 }\n              : { duration: 0.5, ease: ENTER_EASE }\n          }\n        >\n          {phrases[index]}\n        </motion.span>\n      </AnimatePresence>\n    </span>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/shared-axis-x/index.tsx","type":"registry:ui"}],"name":"shared-axis-x","registryDependencies":[],"title":"Shared Axis X","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"A SharedAxisY text animation component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useEffect, useState } from \"react\";\n\nexport interface SharedAxisYProps {\n  className?: string;\n  /** Interval between phrase swaps in milliseconds. */\n  interval?: number;\n  phrases: string[];\n}\n\nconst STAGGER_S = 0.078;\n\n/**\n * SharedAxisY — per-word hard-cut staircase transition.\n * Each word flips in/out with stepped timing, creating a sharp editorial cascade.\n * From the animate-text catalog (`shared-axis-y`, display: \"Word Cut Staircase\").\n */\nexport default function SharedAxisY({\n  phrases,\n  className = \"\",\n  interval = 2500,\n}: SharedAxisYProps) {\n  const [index, setIndex] = useState(0);\n  const shouldReduceMotion = useReducedMotion();\n\n  useEffect(() => {\n    const id = setInterval(() => {\n      setIndex((prev) => (prev + 1) % phrases.length);\n    }, interval);\n    return () => clearInterval(id);\n  }, [interval, phrases.length]);\n\n  const words = (phrases[index] ?? \"\").split(\" \");\n\n  return (\n    <span\n      aria-live=\"polite\"\n      className={`inline-flex flex-wrap gap-x-[0.25em] ${className}`}\n    >\n      <AnimatePresence mode=\"wait\">\n        <motion.span\n          className=\"inline-flex flex-wrap gap-x-[0.25em]\"\n          key={index}\n        >\n          {words.map((word, i) => (\n            <motion.span\n              animate={{ opacity: 1 }}\n              exit={\n                shouldReduceMotion\n                  ? { opacity: 0, transition: { duration: 0 } }\n                  : {\n                      opacity: 0,\n                      transition: { delay: i * STAGGER_S, duration: 0 },\n                    }\n              }\n              initial={shouldReduceMotion ? { opacity: 1 } : { opacity: 0 }}\n              // biome-ignore lint/suspicious/noArrayIndexKey: words have no stable id\n              key={i}\n              style={{ display: \"inline-block\" }}\n              transition={\n                shouldReduceMotion\n                  ? { duration: 0 }\n                  : { delay: i * STAGGER_S, duration: 0 }\n              }\n            >\n              {word}\n            </motion.span>\n          ))}\n        </motion.span>\n      </AnimatePresence>\n    </span>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/shared-axis-y/index.tsx","type":"registry:ui"}],"name":"shared-axis-y","registryDependencies":[],"title":"Shared Axis Y","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"A SharedAxisZ text animation component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useEffect, useState } from \"react\";\n\nexport interface SharedAxisZProps {\n  className?: string;\n  /** Interval between phrase swaps in milliseconds. */\n  interval?: number;\n  phrases: string[];\n}\n\nconst ENTER_EASE = [0.2, 0, 0, 1] as const;\nconst EXIT_EASE = [0.4, 0, 1, 1] as const;\n\n/**\n * SharedAxisZ — scale/depth phrase transition inspired by Google Material shared axis (Z).\n * Incoming phrase scales up from 0.9 with a soft blur; outgoing scales to 1.06 and fades.\n * From the animate-text catalog (`shared-axis-z`).\n */\nexport default function SharedAxisZ({\n  phrases,\n  className = \"\",\n  interval = 2500,\n}: SharedAxisZProps) {\n  const [index, setIndex] = useState(0);\n  const shouldReduceMotion = useReducedMotion();\n\n  useEffect(() => {\n    const id = setInterval(() => {\n      setIndex((prev) => (prev + 1) % phrases.length);\n    }, interval);\n    return () => clearInterval(id);\n  }, [interval, phrases.length]);\n\n  return (\n    <span\n      aria-live=\"polite\"\n      className={`relative inline-block overflow-hidden ${className}`}\n    >\n      <AnimatePresence mode=\"wait\">\n        <motion.span\n          animate={\n            shouldReduceMotion\n              ? { opacity: 1 }\n              : { filter: \"blur(0px)\", opacity: 1, scale: 1 }\n          }\n          exit={\n            shouldReduceMotion\n              ? { opacity: 0, transition: { duration: 0 } }\n              : {\n                  filter: \"blur(1px)\",\n                  opacity: 0,\n                  scale: 1.06,\n                  transition: { duration: 0.36, ease: EXIT_EASE },\n                }\n          }\n          initial={\n            shouldReduceMotion\n              ? { opacity: 1 }\n              : { filter: \"blur(2px)\", opacity: 0, scale: 0.9 }\n          }\n          key={index}\n          style={{ display: \"inline-block\" }}\n          transition={\n            shouldReduceMotion\n              ? { duration: 0 }\n              : { duration: 0.52, ease: ENTER_EASE }\n          }\n        >\n          {phrases[index]}\n        </motion.span>\n      </AnimatePresence>\n    </span>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/shared-axis-z/index.tsx","type":"registry:ui"}],"name":"shared-axis-z","registryDependencies":[],"title":"Shared Axis Z","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"A ShimmerSweep text animation component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { motion, useInView, useReducedMotion } from \"motion/react\";\nimport { useRef } from \"react\";\n\nexport interface ShimmerSweepProps {\n  children: string;\n  className?: string;\n  /** Delay before the animation starts, in milliseconds. */\n  delay?: number;\n  /** Animate only once the text scrolls into view. */\n  triggerOnView?: boolean;\n}\n\nconst DURATION_S = 0.85;\nconst MS = 1000;\n// Apple's signature ease-out.\nconst EASE = [0.22, 1, 0.36, 1] as const;\n\n/**\n * ShimmerSweep — whole-text fade-in with a left-to-center slide and blur\n * dissolve. A subtle sweep across a clean headline, blending in while gliding\n * from left to center. From the animate-text catalog (`shimmer-sweep`).\n * Best on hero titles and section headings.\n */\nconst ShimmerSweep = ({\n  children,\n  className = \"\",\n  delay = 0,\n  triggerOnView = false,\n}: ShimmerSweepProps) => {\n  const ref = useRef<HTMLSpanElement>(null);\n  const inView = useInView(ref, { once: true });\n  const shouldReduceMotion = useReducedMotion();\n  const play = (!triggerOnView || inView) && !shouldReduceMotion;\n\n  return (\n    <span aria-label={children} className={className} ref={ref}>\n      <motion.span\n        animate={play ? { filter: \"blur(0px)\", opacity: 1, x: 0 } : undefined}\n        aria-hidden=\"true\"\n        initial={\n          shouldReduceMotion\n            ? { filter: \"blur(0px)\", opacity: 1, x: 0 }\n            : { filter: \"blur(8px)\", opacity: 0, x: -22 }\n        }\n        style={{ display: \"inline-block\" }}\n        transition={\n          shouldReduceMotion\n            ? { duration: 0 }\n            : {\n                delay: delay / MS,\n                duration: DURATION_S,\n                ease: EASE,\n              }\n        }\n      >\n        {children}\n      </motion.span>\n    </span>\n  );\n};\n\nexport default ShimmerSweep;\n","path":"index.tsx","target":"components/smoothui/shimmer-sweep/index.tsx","type":"registry:ui"}],"name":"shimmer-sweep","registryDependencies":[],"title":"Shimmer Sweep","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"A ShineText light-sweep text animation component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { motion, useReducedMotion } from \"motion/react\";\n\nexport interface ShineTextProps {\n  /** Base text color (any CSS color). */\n  baseColor?: string;\n  children: string;\n  className?: string;\n  /** Duration of one sweep, in seconds. */\n  duration?: number;\n  /** Pause between sweeps, in seconds. */\n  repeatDelay?: number;\n  /** Color of the sweeping highlight band. */\n  shineColor?: string;\n}\n\n/**\n * ShineText — a literal light sweep that glides across the text on a loop,\n * using a clipped moving gradient (metallic \"shine\" / shimmer). For the\n * catalog's fade+slide reveal see `shimmer-sweep`.\n */\nexport default function ShineText({\n  children,\n  className = \"\",\n  duration = 2.5,\n  repeatDelay = 0.6,\n  baseColor = \"var(--color-muted-foreground)\",\n  shineColor = \"var(--color-foreground)\",\n}: ShineTextProps) {\n  const shouldReduceMotion = useReducedMotion();\n\n  if (shouldReduceMotion) {\n    return (\n      <span className={className} style={{ color: baseColor }}>\n        {children}\n      </span>\n    );\n  }\n\n  return (\n    <motion.span\n      animate={{ backgroundPositionX: [\"150%\", \"-150%\"] }}\n      className={className}\n      style={{\n        backgroundClip: \"text\",\n        backgroundImage: `linear-gradient(110deg, ${baseColor} 40%, ${shineColor} 50%, ${baseColor} 60%)`,\n        backgroundSize: \"250% 100%\",\n        color: \"transparent\",\n        WebkitBackgroundClip: \"text\",\n        WebkitTextFillColor: \"transparent\",\n      }}\n      transition={{\n        duration,\n        ease: \"linear\",\n        repeat: Number.POSITIVE_INFINITY,\n        repeatDelay,\n      }}\n    >\n      {children}\n    </motion.span>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/shine-text/index.tsx","type":"registry:ui"}],"name":"shine-text","registryDependencies":[],"title":"Shine Text","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"A ShortSlideDown text animation component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useEffect, useState } from \"react\";\n\nexport interface ShortSlideDownProps {\n  className?: string;\n  /** Interval between phrase completions in milliseconds. */\n  interval?: number;\n  phrases: string[];\n}\n\nconst BUILD_EASE = [0.2, 0.8, 0.2, 1] as const;\nconst EXIT_EASE = [0.4, 0, 0.2, 1] as const;\n\n/**\n * ShortSlideDown — each new word drops in from above into its own centered line,\n * pushing the stack downward until a centered multi-line composition locks in place.\n * From the animate-text catalog (`short-slide-down`).\n */\nexport default function ShortSlideDown({\n  phrases,\n  className = \"\",\n  interval = 2500,\n}: ShortSlideDownProps) {\n  const [phraseIndex, setPhraseIndex] = useState(0);\n  const [wordCount, setWordCount] = useState(1);\n  const [exiting, setExiting] = useState(false);\n  const shouldReduceMotion = useReducedMotion();\n\n  const currentPhrase = phrases[phraseIndex] ?? \"\";\n  const words = currentPhrase.split(\" \");\n\n  // biome-ignore lint/correctness/useExhaustiveDependencies: words.length drives the build schedule\n  useEffect(() => {\n    if (shouldReduceMotion) {\n      const holdId = setTimeout(() => {\n        setPhraseIndex((prev) => (prev + 1) % phrases.length);\n      }, interval);\n      return () => clearTimeout(holdId);\n    }\n\n    setWordCount(1);\n    setExiting(false);\n\n    const buildTimers: ReturnType<typeof setTimeout>[] = [];\n\n    for (let i = 1; i < words.length; i++) {\n      buildTimers.push(\n        setTimeout(() => {\n          setWordCount(i + 1);\n        }, i * 500)\n      );\n    }\n\n    const totalBuild = (words.length - 1) * 500 + 360;\n    const holdId = setTimeout(() => {\n      setExiting(true);\n      setTimeout(() => {\n        setPhraseIndex((prev) => (prev + 1) % phrases.length);\n        setWordCount(1);\n        setExiting(false);\n      }, 320 + 180);\n    }, totalBuild + interval);\n\n    buildTimers.push(holdId);\n    return () => {\n      for (const t of buildTimers) {\n        clearTimeout(t);\n      }\n    };\n  }, [phraseIndex, phrases.length, interval, shouldReduceMotion, words.length]);\n\n  const visibleWords = shouldReduceMotion ? words : words.slice(0, wordCount);\n  const restingAnimate = shouldReduceMotion\n    ? { opacity: 1 }\n    : { filter: \"blur(0px)\", opacity: 1, scale: 1, y: 0 };\n  const exitAnimate = {\n    filter: \"blur(1.2px)\",\n    opacity: 0,\n    transition: { duration: 0.32, ease: EXIT_EASE },\n    y: 10,\n  };\n\n  return (\n    <span\n      aria-live=\"polite\"\n      className={`flex flex-col items-center ${className}`}\n      style={{ gap: 12 }}\n    >\n      <AnimatePresence mode=\"popLayout\">\n        {visibleWords.map((word, i) => (\n          <motion.span\n            animate={exiting ? exitAnimate : restingAnimate}\n            initial={\n              shouldReduceMotion\n                ? { opacity: 1 }\n                : { filter: \"blur(2.4px)\", opacity: 0, scale: 0.992, y: -28 }\n            }\n            // biome-ignore lint/suspicious/noArrayIndexKey: word position is the stable identity within a phrase\n            key={`${phraseIndex}-${i}`}\n            layout\n            style={{ display: \"block\" }}\n            transition={\n              shouldReduceMotion\n                ? { duration: 0 }\n                : {\n                    duration: i === 0 ? 0.36 : 0.5,\n                    ease: BUILD_EASE,\n                    layout: { duration: 0.5, ease: BUILD_EASE },\n                  }\n            }\n          >\n            {word}\n          </motion.span>\n        ))}\n      </AnimatePresence>\n    </span>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/short-slide-down/index.tsx","type":"registry:ui"}],"name":"short-slide-down","registryDependencies":[],"title":"Short Slide Down","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"A ShortSlideRight text animation component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useEffect, useState } from \"react\";\n\nexport interface ShortSlideRightProps {\n  className?: string;\n  /** Interval between phrase swaps in milliseconds. */\n  interval?: number;\n  phrases: string[];\n}\n\nconst ENTER_EASE = [0.2, 0.8, 0.2, 1] as const;\nconst EXIT_EASE = [0.4, 0, 0.2, 1] as const;\n\n/**\n * ShortSlideRight — the whole phrase glides in from the left as one compact unit\n * while words reveal in sequence through opacity stagger.\n * Keynote-style editorial restraint. From the animate-text catalog (`short-slide-right`).\n */\nexport default function ShortSlideRight({\n  phrases,\n  className = \"\",\n  interval = 2500,\n}: ShortSlideRightProps) {\n  const [index, setIndex] = useState(0);\n  const shouldReduceMotion = useReducedMotion();\n\n  useEffect(() => {\n    const id = setInterval(() => {\n      setIndex((prev) => (prev + 1) % phrases.length);\n    }, interval);\n    return () => clearInterval(id);\n  }, [interval, phrases.length]);\n\n  const words = phrases[index]?.split(\" \") ?? [];\n\n  return (\n    <span\n      aria-live=\"polite\"\n      className={`relative inline-block overflow-hidden ${className}`}\n    >\n      <AnimatePresence mode=\"wait\">\n        <motion.span\n          animate={\n            shouldReduceMotion ? { opacity: 1 } : { filter: \"blur(0px)\", x: 0 }\n          }\n          exit={\n            shouldReduceMotion\n              ? { opacity: 0, transition: { duration: 0 } }\n              : {\n                  filter: \"blur(1px)\",\n                  opacity: 0,\n                  transition: { duration: 0.32, ease: EXIT_EASE },\n                  x: 12,\n                }\n          }\n          initial={\n            shouldReduceMotion\n              ? { opacity: 1 }\n              : { filter: \"blur(1.2px)\", x: -24 }\n          }\n          key={index}\n          style={{ display: \"inline-flex\", flexWrap: \"nowrap\", gap: \"0.25em\" }}\n          transition={\n            shouldReduceMotion\n              ? { duration: 0 }\n              : { duration: 0.52, ease: ENTER_EASE }\n          }\n        >\n          {words.map((word, i) => (\n            <motion.span\n              animate={{ opacity: 1 }}\n              initial={shouldReduceMotion ? { opacity: 1 } : { opacity: 0 }}\n              // biome-ignore lint/suspicious/noArrayIndexKey: words have no stable id\n              key={i}\n              style={{ display: \"inline-block\" }}\n              transition={\n                shouldReduceMotion\n                  ? { duration: 0 }\n                  : { delay: i * 0.092, duration: 0.21, ease: ENTER_EASE }\n              }\n            >\n              {word}\n            </motion.span>\n          ))}\n        </motion.span>\n      </AnimatePresence>\n    </span>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/short-slide-right/index.tsx","type":"registry:ui"}],"name":"short-slide-right","registryDependencies":[],"title":"Short Slide Right","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"A beautiful animated orb component inspired by Siri's visual design.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport {\n  type MotionStyle,\n  motion,\n  type TargetAndTransition,\n  type Transition,\n  useReducedMotion,\n  useTransform,\n} from \"motion/react\";\nimport {\n  type AIAmplitude,\n  type AIState,\n  getAIStateMotion,\n  useAmplitudeValue,\n} from \"../ai-core\";\n\nconst SIZE_THRESHOLD_SMALL = 50;\nconst SIZE_THRESHOLD_TINY = 30;\nconst SIZE_THRESHOLD_MEDIUM = 100;\nconst BLUR_MULTIPLIER_SMALL = 0.008;\nconst BLUR_MIN_SMALL = 1;\nconst BLUR_MULTIPLIER_LARGE = 0.015;\nconst BLUR_MIN_LARGE = 4;\nconst CONTRAST_MULTIPLIER_SMALL = 0.004;\nconst CONTRAST_MIN_SMALL = 1.2;\nconst CONTRAST_MULTIPLIER_LARGE = 0.008;\nconst CONTRAST_MIN_LARGE = 1.5;\nconst DOT_SIZE_MULTIPLIER_SMALL = 0.004;\nconst DOT_SIZE_MIN_SMALL = 0.05;\nconst DOT_SIZE_MULTIPLIER_LARGE = 0.008;\nconst DOT_SIZE_MIN_LARGE = 0.1;\nconst SHADOW_MULTIPLIER_SMALL = 0.004;\nconst SHADOW_MIN_SMALL = 0.5;\nconst SHADOW_MULTIPLIER_LARGE = 0.008;\nconst SHADOW_MIN_LARGE = 2;\nconst MASK_RADIUS_TINY = \"0%\";\nconst MASK_RADIUS_SMALL = \"5%\";\nconst MASK_RADIUS_MEDIUM = \"15%\";\nconst MASK_RADIUS_LARGE = \"25%\";\nconst CONTRAST_TINY = 1.1;\nconst CONTRAST_MULTIPLIER_FINAL = 1.2;\nconst CONTRAST_MIN_FINAL = 1.3;\n\n/** Loud audio tightens the gradient, which reads as the orb \"focusing\". */\nconst AMPLITUDE_BLUR_FALLOFF = 0.45;\n/** Amplitude adds at most 12% of extra size on top of the state scale. */\nconst AMPLITUDE_SCALE_GAIN = 0.12;\n/** Single lateral nudge used to signal `error`, well under 200ms. */\nconst ERROR_SHAKE_KEYFRAMES = [0, -3, 3, 0];\nconst ERROR_SHAKE_DURATION = 0.18;\nconst EASE_IN_OUT = [0.645, 0.045, 0.355, 1] as const;\nconst GLOW_BLUR_RATIO = 0.28;\nconst GLOW_MAX_OPACITY = 0.7;\n/** Depth rim, as a fraction of the orb size. */\nconst RIM_RATIO = 0.06;\nconst RIM_MIN = 1.5;\n/** Sheen drift period at rest, before the state speed divides it. */\nconst DRIFT_BASE_SECONDS = 12;\n/** `idle` breathes: a slow, shallow scale cycle that never draws attention. */\nconst BREATHE_SCALE = [1, 1.035, 1];\nconst BREATHE_SECONDS = 5.5;\nconst SPRING_DEFAULT: Transition = {\n  bounce: 0.1,\n  duration: 0.25,\n  type: \"spring\",\n};\n\nexport interface SiriOrbProps {\n  /**\n   * Live audio level, 0–1. Pass the `MotionValue` from `useAudioAmplitude` so\n   * the signal never re-renders React.\n   */\n  amplitude?: AIAmplitude;\n  /** Ambient rotation period in seconds, before the state speed multiplier. */\n  animationDuration?: number;\n  className?: string;\n  colors?: {\n    bg?: string;\n    c1?: string;\n    c2?: string;\n    c3?: string;\n    /** Fourth mesh stop. More stops means fewer visible repeats per rotation. */\n    c4?: string;\n  };\n  size?: string;\n  /** Shared AI state driving speed, scale, saturation and reactivity. */\n  state?: AIState;\n}\n\nconst SiriOrb: React.FC<SiriOrbProps> = ({\n  size = \"192px\",\n  className,\n  colors,\n  animationDuration = 20,\n  amplitude,\n  state = \"idle\",\n}) => {\n  const shouldReduceMotion = useReducedMotion();\n  const amplitudeValue = useAmplitudeValue(amplitude);\n  const stateMotion = getAIStateMotion(state);\n\n  /**\n   * Higher chroma than the original pastels.\n   *\n   * The first palette sat at chroma 0.12–0.15, which washes out once the mesh is\n   * blurred and the dot pattern is overlaid on top. Pushing to ~0.2 and adding a\n   * saturate() pass to the mesh is what makes the colour survive all that.\n   */\n  const defaultColors = {\n    bg: \"oklch(92% 0.03 300)\",\n    c1: \"oklch(68% 0.21 350)\", // Pink\n    c2: \"oklch(70% 0.18 210)\", // Blue\n    c3: \"oklch(66% 0.2 285)\", // Violet\n    c4: \"oklch(72% 0.19 325)\", // Magenta, the fourth stop for variety\n  };\n\n  const finalColors = { ...defaultColors, ...colors };\n  // The bloom stays in the orb's own palette — see the note in ai-orb-aura.\n  const glowColor = finalColors.c2;\n\n  // Extract numeric value from size for calculations\n  const sizeValue = Number.parseInt(size.replace(\"px\", \"\"), 10);\n\n  // Responsive calculations based on size\n  const blurAmount =\n    sizeValue < SIZE_THRESHOLD_SMALL\n      ? Math.max(sizeValue * BLUR_MULTIPLIER_SMALL, BLUR_MIN_SMALL) // Reduced blur for small sizes\n      : Math.max(sizeValue * BLUR_MULTIPLIER_LARGE, BLUR_MIN_LARGE);\n\n  const contrastAmount =\n    sizeValue < SIZE_THRESHOLD_SMALL\n      ? Math.max(sizeValue * CONTRAST_MULTIPLIER_SMALL, CONTRAST_MIN_SMALL) // Reduced contrast for small sizes\n      : Math.max(sizeValue * CONTRAST_MULTIPLIER_LARGE, CONTRAST_MIN_LARGE);\n\n  const dotSize =\n    sizeValue < SIZE_THRESHOLD_SMALL\n      ? Math.max(sizeValue * DOT_SIZE_MULTIPLIER_SMALL, DOT_SIZE_MIN_SMALL) // Smaller dots for small sizes\n      : Math.max(sizeValue * DOT_SIZE_MULTIPLIER_LARGE, DOT_SIZE_MIN_LARGE);\n\n  const shadowSpread =\n    sizeValue < SIZE_THRESHOLD_SMALL\n      ? Math.max(sizeValue * SHADOW_MULTIPLIER_SMALL, SHADOW_MIN_SMALL) // Reduced shadow for small sizes\n      : Math.max(sizeValue * SHADOW_MULTIPLIER_LARGE, SHADOW_MIN_LARGE);\n\n  // Adjust mask radius based on size to reduce black center in small sizes\n  const getMaskRadius = (value: number) => {\n    if (value < SIZE_THRESHOLD_TINY) {\n      return MASK_RADIUS_TINY;\n    }\n    if (value < SIZE_THRESHOLD_SMALL) {\n      return MASK_RADIUS_SMALL;\n    }\n    if (value < SIZE_THRESHOLD_MEDIUM) {\n      return MASK_RADIUS_MEDIUM;\n    }\n    return MASK_RADIUS_LARGE;\n  };\n\n  const maskRadius = getMaskRadius(sizeValue);\n\n  // Use more subtle contrast for very small sizes\n  const getFinalContrast = (value: number) => {\n    if (value < SIZE_THRESHOLD_TINY) {\n      return CONTRAST_TINY; // Very subtle contrast for tiny sizes\n    }\n    if (value < SIZE_THRESHOLD_SMALL) {\n      return Math.max(\n        contrastAmount * CONTRAST_MULTIPLIER_FINAL,\n        CONTRAST_MIN_FINAL\n      ); // Reduced contrast for small sizes\n    }\n    return contrastAmount;\n  };\n\n  const finalContrast = getFinalContrast(sizeValue);\n\n  // Reactivity is gated by the state preset: `thinking` barely listens so the\n  // orb keeps churning internally instead of throbbing with room noise.\n  const reactivity = shouldReduceMotion ? 0 : stateMotion.reactivity;\n\n  const reactiveBlur = useTransform(amplitudeValue, (level) => {\n    const focus = 1 - level * reactivity * AMPLITUDE_BLUR_FALLOFF;\n    return `${blurAmount * focus}px`;\n  });\n\n  const reactiveScale = useTransform(\n    amplitudeValue,\n    (level) => stateMotion.scale + level * reactivity * AMPLITUDE_SCALE_GAIN\n  );\n\n  const loopDuration = shouldReduceMotion\n    ? animationDuration\n    : animationDuration / stateMotion.speed;\n\n  // Rim thickness scales with the orb, so the lit edge reads the same at 24px\n  // and at 240px instead of swallowing the small one.\n  const rim = Math.max(sizeValue * RIM_RATIO, RIM_MIN);\n  // The sheen drifts slower than the mesh rotates; a busier state speeds it up\n  // a touch without matching the rotation, which would look mechanical.\n  const driftDuration = (DRIFT_BASE_SECONDS / (1 + stateMotion.speed)) * 2;\n\n  const getRootAnimate = (): TargetAndTransition => {\n    if (shouldReduceMotion) {\n      return { scale: 1, x: 0 };\n    }\n    if (state === \"error\") {\n      return { scale: 1, x: ERROR_SHAKE_KEYFRAMES };\n    }\n    if (stateMotion.motif === \"breathe\") {\n      return { scale: BREATHE_SCALE, x: 0 };\n    }\n    return { scale: 1, x: 0 };\n  };\n\n  const getRootTransition = (): Transition => {\n    if (shouldReduceMotion) {\n      return { duration: 0 };\n    }\n    if (state === \"error\") {\n      return { duration: ERROR_SHAKE_DURATION, ease: EASE_IN_OUT };\n    }\n    if (stateMotion.motif === \"breathe\") {\n      return {\n        duration: BREATHE_SECONDS,\n        ease: EASE_IN_OUT,\n        repeat: Number.POSITIVE_INFINITY,\n      };\n    }\n    return SPRING_DEFAULT;\n  };\n\n  return (\n    // The gradient disc clips its own overflow, so the bloom and the status\n    // motif have to live in a wrapper rather than inside it.\n    <motion.div\n      animate={getRootAnimate()}\n      className={cn(\"relative\", className)}\n      style={\n        {\n          \"--orb-size\": size,\n          height: size,\n          width: size,\n        } as MotionStyle\n      }\n      transition={getRootTransition()}\n    >\n      {/* Bloom tinted by the state's semantic accent. */}\n      <motion.div\n        animate={{ opacity: stateMotion.glow * GLOW_MAX_OPACITY }}\n        className=\"absolute rounded-full\"\n        style={{\n          background: `radial-gradient(circle at 50% 50%, ${glowColor} 0%, transparent 64%)`,\n          filter: `blur(calc(var(--orb-size) * ${GLOW_BLUR_RATIO}))`,\n          inset: \"-12%\",\n        }}\n        transition={shouldReduceMotion ? { duration: 0 } : SPRING_DEFAULT}\n      />\n\n      <motion.div\n        className=\"siri-orb\"\n        style={\n          {\n            \"--animation-duration\": `${loopDuration}s`,\n            \"--bg\": finalColors.bg,\n            \"--blur-amount\": reactiveBlur,\n            \"--c1\": finalColors.c1,\n            \"--c2\": finalColors.c2,\n            \"--c3\": finalColors.c3,\n            \"--c4\": finalColors.c4,\n            \"--contrast-amount\": finalContrast,\n            \"--dot-size\": `${dotSize}px`,\n            \"--drift-duration\": `${driftDuration}s`,\n            \"--mask-radius\": maskRadius,\n            \"--rim\": `${rim}px`,\n            \"--shadow-spread\": `${shadowSpread}px`,\n            // Saturation and hue only affect the gradient disc. Applying them\n            // to the whole component would desaturate the semantic accent too,\n            // and `error` would lose the very red that identifies it.\n            filter: `saturate(${stateMotion.saturation}) hue-rotate(${stateMotion.hueRotate}deg)`,\n            height: \"100%\",\n            scale: reactiveScale,\n            width: \"100%\",\n          } as MotionStyle\n        }\n      >\n        {/* Specular sheen and depth rim. The mesh alone reads as a flat blurred\n            disc; a drifting highlight plus a lit top edge and shaded bottom are\n            what make it read as a sphere. */}\n        <span aria-hidden=\"true\" className=\"siri-orb-layer siri-orb-sheen\" />\n        <span aria-hidden=\"true\" className=\"siri-orb-layer siri-orb-rim\" />\n        <style>{`\n        @property --angle {\n          syntax: \"<angle>\";\n          inherits: false;\n          initial-value: 0deg;\n        }\n\n        .siri-orb {\n          display: grid;\n          grid-template-areas: \"stack\";\n          overflow: hidden;\n          border-radius: 50%;\n          position: relative;\n          isolation: isolate;\n        }\n\n        .siri-orb::before,\n        .siri-orb::after,\n        .siri-orb > .siri-orb-layer {\n          content: \"\";\n          display: block;\n          grid-area: stack;\n          width: 100%;\n          height: 100%;\n          border-radius: 50%;\n        }\n\n        /* Glassy highlight that drifts, so the sheen never sits still enough to\n           read as a printed-on gradient. */\n        .siri-orb-sheen {\n          background:\n            radial-gradient(circle at 30% 24%, hsl(0 0% 100% / 0.32), transparent 34%),\n            radial-gradient(circle at 72% 80%, hsl(0 0% 100% / 0.07), transparent 48%);\n          mix-blend-mode: screen;\n          animation: siri-drift var(--drift-duration) ease-in-out infinite alternate;\n        }\n\n        /* Lit top edge, shaded bottom, thin inner ring. */\n        .siri-orb-rim {\n          box-shadow:\n            inset 0 0 0 1px hsl(0 0% 100% / 0.16),\n            inset 0 calc(var(--rim) * 1) calc(var(--rim) * 2) hsl(0 0% 100% / 0.22),\n            inset 0 calc(var(--rim) * -1.2) calc(var(--rim) * 2.4) hsl(0 0% 0% / 0.4);\n          pointer-events: none;\n        }\n\n        @keyframes siri-drift {\n          0% { transform: translate(-6%, -4%) scale(1.05); }\n          100% { transform: translate(7%, 6%) scale(1.12); }\n        }\n\n        .siri-orb::before {\n          background:\n            conic-gradient(\n              from calc(var(--angle) * 2) at 25% 70%,\n              var(--c3),\n              transparent 20% 80%,\n              var(--c3)\n            ),\n            conic-gradient(\n              from calc(var(--angle) * 2) at 45% 75%,\n              var(--c2),\n              transparent 30% 60%,\n              var(--c2)\n            ),\n            conic-gradient(\n              from calc(var(--angle) * -3) at 80% 20%,\n              var(--c1),\n              transparent 40% 60%,\n              var(--c1)\n            ),\n            conic-gradient(\n              from calc(var(--angle) * 1.5) at 60% 35%,\n              var(--c4),\n              transparent 25% 75%,\n              var(--c4)\n            ),\n            conic-gradient(\n              from calc(var(--angle) * 2) at 15% 5%,\n              var(--c2),\n              transparent 10% 90%,\n              var(--c2)\n            ),\n            conic-gradient(\n              from calc(var(--angle) * 1) at 20% 80%,\n              var(--c1),\n              transparent 10% 90%,\n              var(--c1)\n            ),\n            conic-gradient(\n              from calc(var(--angle) * -2) at 85% 10%,\n              var(--c3),\n              transparent 20% 80%,\n              var(--c3)\n            );\n          box-shadow: inset var(--bg) 0 0 var(--shadow-spread)\n            calc(var(--shadow-spread) * 0.2);\n          /* The saturate() pass is what keeps the colour alive after the blur\n             and the overlaid dot pattern have both eaten into it. */\n          filter: blur(var(--blur-amount)) contrast(var(--contrast-amount))\n            saturate(1.4);\n          animation: rotate var(--animation-duration) linear infinite;\n        }\n\n        .siri-orb::after {\n          background-image: radial-gradient(\n            circle at center,\n            var(--bg) var(--dot-size),\n            transparent var(--dot-size)\n          );\n          background-size: calc(var(--dot-size) * 2) calc(var(--dot-size) * 2);\n          backdrop-filter: blur(calc(var(--blur-amount) * 2))\n            contrast(calc(var(--contrast-amount) * 2));\n          mix-blend-mode: overlay;\n        }\n\n        /* Apply mask only when radius is greater than 0 */\n        .siri-orb[style*=\"--mask-radius: 0%\"]::after {\n          mask-image: none;\n        }\n\n        .siri-orb:not([style*=\"--mask-radius: 0%\"])::after {\n          mask-image: radial-gradient(\n            black var(--mask-radius),\n            transparent 75%\n          );\n        }\n\n        @keyframes rotate {\n          to {\n            --angle: 360deg;\n          }\n        }\n\n        @media (prefers-reduced-motion: reduce) {\n          .siri-orb::before,\n          .siri-orb-sheen {\n            animation: none;\n          }\n        }\n      `}</style>\n      </motion.div>\n    </motion.div>\n  );\n};\n\nexport default SiriOrb;\n","path":"index.tsx","target":"components/smoothui/siri-orb/index.tsx","type":"registry:ui"}],"name":"siri-orb","registryDependencies":["https://smoothui.dev/r/ai-core.json"],"title":"Siri Orb","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":[],"description":"Animated skeleton loading placeholders","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport type { ReactNode } from \"react\";\n\nexport interface SkeletonProps {\n  /** Content to wrap - skeleton will match its dimensions */\n  children?: ReactNode;\n  /** Additional classes */\n  className?: string;\n  /** When true, shows skeleton effect. When false, shows children */\n  loading?: boolean;\n}\n\nconst Skeleton = ({ loading = true, children, className }: SkeletonProps) => {\n  // If not loading and has children, just render children\n  if (!loading && children) {\n    return <>{children}</>;\n  }\n\n  // If loading with children, wrap them and apply skeleton effect\n  if (loading && children) {\n    return (\n      <div\n        aria-busy=\"true\"\n        aria-live=\"polite\"\n        className={cn(\"relative\", className)}\n      >\n        {/* Children are invisible but maintain layout */}\n        <div className=\"invisible\">{children}</div>\n        {/* Skeleton overlay that matches children's dimensions */}\n        <div\n          aria-hidden=\"true\"\n          className=\"absolute inset-0 animate-pulse rounded-[inherit] bg-muted-foreground/20\"\n        />\n      </div>\n    );\n  }\n\n  // Basic skeleton block (no children)\n  return (\n    <div\n      aria-busy=\"true\"\n      className={cn(\n        \"animate-pulse rounded-md bg-muted-foreground/20\",\n        className\n      )}\n    />\n  );\n};\n\nexport default Skeleton;\n","path":"index.tsx","target":"components/smoothui/skeleton-loader/index.tsx","type":"registry:ui"}],"name":"skeleton-loader","registryDependencies":[],"title":"Skeleton Loader","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["@radix-ui/react-slot","class-variance-authority","motion"],"description":"A polished button component with gradient variants and press animation","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cn } from \"@/lib/utils\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport type { ButtonHTMLAttributes, ReactNode, Ref } from \"react\";\nimport { useEffect, useRef } from \"react\";\n\n/**\n * SmoothButton — the first primitive of the SmoothUI Design System.\n *\n * Three orthogonal axes (see design-system/components/button.md):\n *   variant → appearance only (solid | soft | outline | ghost | link | candy)\n *   color   → hue (accent | neutral | destructive | blue | amber | green)\n *   size    → xs | sm | default | lg + icon-*\n *\n * The `color` axis only sets CSS custom props (--btn, --btn-hover, --btn-fg);\n * each `variant` consumes them generically, so 6 variants × 6 colors stay 12\n * class strings, not 36. When no `color` is given, CSS var fallbacks apply\n * (candy → brand, everything else → neutral/foreground).\n *\n * Legacy variants `default` / `secondary` / `destructive` are preserved verbatim\n * for back-compat with existing call sites and ignore the `color` axis.\n */\nconst smoothButtonVariants = cva(\n  \"relative inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap font-medium outline-none ring-offset-background transition-[transform,background-color,border-color,color,box-shadow] duration-150 ease-out focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 active:scale-[0.97] disabled:pointer-events-none disabled:opacity-50 motion-reduce:transition-none motion-reduce:active:scale-100 [&_svg]:pointer-events-none [&_svg]:shrink-0\",\n  {\n    defaultVariants: {\n      shape: \"default\",\n      size: \"default\",\n      variant: \"default\",\n    },\n    variants: {\n      color: {\n        accent:\n          \"[--btn-fg:#fff] [--btn-hover:var(--color-brand-secondary)] [--btn:var(--color-brand)]\",\n        amber:\n          \"[--btn-fg:var(--color-amber-fg)] [--btn-hover:var(--color-amber-hover)] [--btn:var(--color-amber)]\",\n        blue: \"[--btn-fg:var(--color-blue-fg)] [--btn-hover:var(--color-blue-hover)] [--btn:var(--color-blue)]\",\n        destructive:\n          \"[--btn-fg:#fff] [--btn-hover:color-mix(in_oklab,var(--color-destructive)_85%,black)] [--btn:var(--color-destructive)]\",\n        green:\n          \"[--btn-fg:var(--color-green-fg)] [--btn-hover:var(--color-green-hover)] [--btn:var(--color-green)]\",\n        neutral:\n          \"[--btn-fg:var(--color-background)] [--btn-hover:var(--color-smooth-900)] [--btn:var(--color-foreground)]\",\n      },\n      shape: {\n        default: \"\",\n        pill: \"rounded-full!\",\n        square: \"rounded-none!\",\n      },\n      size: {\n        default: \"h-10 gap-2 rounded-md px-4 py-2 text-sm [&_svg]:size-4\",\n        icon: \"size-10 rounded-md [&_svg]:size-4\",\n        \"icon-lg\": \"size-11 rounded-lg [&_svg]:size-5\",\n        \"icon-sm\": \"size-9 rounded-md [&_svg]:size-4\",\n        lg: \"h-11 gap-2 rounded-lg px-8 text-base [&_svg]:size-5\",\n        sm: \"h-9 gap-1.5 rounded-md px-3 text-sm [&_svg]:size-4\",\n        xs: \"h-7 gap-1.5 rounded-sm px-2.5 text-xs [&_svg]:size-3.5\",\n      },\n      variant: {\n        candy:\n          \"border-[0.5px] border-white/25 bg-gradient-to-b from-[var(--btn,var(--color-brand))] to-[var(--btn-hover,var(--color-brand-secondary))] text-[var(--btn-fg,#fff)] text-shadow-sm shadow-black/20 shadow-md ring-1 ring-[color-mix(in_oklab,var(--color-foreground)_15%,var(--btn,var(--color-brand)))] hover:from-[var(--btn-hover,var(--color-brand-secondary))] hover:to-[var(--btn-hover,var(--color-brand-secondary))] [&_svg]:drop-shadow-sm\",\n        // --- legacy (preserved verbatim, ignore `color`) ---\n        default:\n          \"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90\",\n        destructive:\n          \"bg-gradient-to-b from-[#FD4B4E] to-destructive text-shadow-sm text-white shadow-[0px_1px_2px_rgba(0,0,0,0.4),0px_0px_0px_1px_#F61418,inset_0px_0.75px_0px_rgba(255,255,255,0.2)] hover:from-destructive hover:to-destructive\",\n        ghost:\n          \"text-[var(--btn,var(--color-foreground))] hover:bg-[color-mix(in_oklab,var(--btn,var(--color-foreground))_10%,transparent)]\",\n        link: \"text-[var(--btn,var(--color-foreground))] underline-offset-4 hover:underline\",\n        outline:\n          \"border border-transparent bg-background text-[var(--btn,var(--color-foreground))] shadow-black/15 shadow-sm ring-1 ring-foreground/10 hover:bg-primary dark:ring-foreground/15\",\n        secondary:\n          \"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80\",\n        soft: \"bg-[color-mix(in_oklab,var(--btn,var(--color-foreground))_12%,transparent)] text-[var(--btn,var(--color-foreground))] hover:bg-[color-mix(in_oklab,var(--btn,var(--color-foreground))_18%,transparent)]\",\n        // --- new decoupled system (consume --btn / --btn-hover / --btn-fg) ---\n        solid:\n          \"bg-[var(--btn,var(--color-foreground))] text-[var(--btn-fg,var(--color-background))] shadow-xs hover:bg-[var(--btn-hover,var(--color-smooth-900))]\",\n      },\n    },\n  }\n);\n\nexport type SmoothButtonProps = Omit<\n  ButtonHTMLAttributes<HTMLButtonElement>,\n  \"prefix\" | \"color\"\n> &\n  VariantProps<typeof smoothButtonVariants> & {\n    asChild?: boolean;\n    /** Show a spinner that morphs the button width without layout jump. */\n    loading?: boolean;\n    /** Content before the label (icon, Kbd…). */\n    prefix?: ReactNode;\n    /** Content after the label. */\n    suffix?: ReactNode;\n    /** Opt-in Safari force-press depth (scales to 0.94 under pressure). */\n    forcePress?: boolean;\n    ref?: Ref<HTMLButtonElement>;\n  };\n\nconst Spinner = () => (\n  <svg\n    aria-hidden=\"true\"\n    className=\"size-[1em] animate-spin\"\n    fill=\"none\"\n    viewBox=\"0 0 24 24\"\n  >\n    <circle\n      className=\"opacity-25\"\n      cx=\"12\"\n      cy=\"12\"\n      r=\"10\"\n      stroke=\"currentColor\"\n      strokeWidth=\"3\"\n    />\n    <path\n      className=\"opacity-90\"\n      d=\"M12 2a10 10 0 0 1 10 10\"\n      stroke=\"currentColor\"\n      strokeLinecap=\"round\"\n      strokeWidth=\"3\"\n    />\n  </svg>\n);\n\nfunction SmoothButton({\n  className,\n  variant,\n  color,\n  size,\n  shape,\n  asChild = false,\n  loading = false,\n  forcePress = false,\n  prefix,\n  suffix,\n  disabled,\n  children,\n  ref,\n  ...props\n}: SmoothButtonProps) {\n  const shouldReduceMotion = useReducedMotion();\n  const localRef = useRef<HTMLButtonElement>(null);\n\n  // Safari-only force-press: deepen the press past the normal active scale.\n  useEffect(() => {\n    const node = localRef.current;\n    if (!(forcePress && node) || shouldReduceMotion) {\n      return;\n    }\n    const FORCE_THRESHOLD = 2; // Safari's \"force click\" boundary\n    const onForce = (e: Event) => {\n      const force = (e as Event & { webkitForce?: number }).webkitForce ?? 0;\n      node.style.transform = force >= FORCE_THRESHOLD ? \"scale(0.94)\" : \"\";\n    };\n    const reset = () => {\n      node.style.transform = \"\";\n    };\n    node.addEventListener(\"webkitmouseforcechanged\", onForce);\n    node.addEventListener(\"mouseup\", reset);\n    node.addEventListener(\"mouseleave\", reset);\n    return () => {\n      node.removeEventListener(\"webkitmouseforcechanged\", onForce);\n      node.removeEventListener(\"mouseup\", reset);\n      node.removeEventListener(\"mouseleave\", reset);\n    };\n  }, [forcePress, shouldReduceMotion]);\n\n  const classes = cn(\n    smoothButtonVariants({ className, color, shape, size, variant })\n  );\n\n  // asChild defers all rendering to the consumer's element — slots/loading\n  // are not injected (single-child contract of Radix Slot).\n  if (asChild) {\n    return (\n      <Slot className={classes} ref={ref} {...props}>\n        {children}\n      </Slot>\n    );\n  }\n\n  const setRefs = (node: HTMLButtonElement | null) => {\n    localRef.current = node;\n    if (typeof ref === \"function\") {\n      ref(node);\n    } else if (ref) {\n      (ref as { current: HTMLButtonElement | null }).current = node;\n    }\n  };\n\n  return (\n    <button\n      aria-busy={loading || undefined}\n      className={classes}\n      disabled={disabled || loading}\n      ref={setRefs}\n      type={props.type ?? \"button\"}\n      {...props}\n    >\n      <AnimatePresence initial={false}>\n        {loading ? (\n          <motion.span\n            animate={{ marginRight: \"0.5rem\", opacity: 1, width: \"1em\" }}\n            className=\"inline-flex shrink-0 items-center justify-center overflow-hidden\"\n            exit={{ marginRight: 0, opacity: 0, width: 0 }}\n            initial={\n              shouldReduceMotion\n                ? { marginRight: \"0.5rem\", opacity: 1, width: \"1em\" }\n                : { marginRight: 0, opacity: 0, width: 0 }\n            }\n            key=\"spinner\"\n            transition={\n              shouldReduceMotion\n                ? { duration: 0 }\n                : { bounce: 0.1, duration: 0.25, type: \"spring\" }\n            }\n          >\n            <Spinner />\n          </motion.span>\n        ) : null}\n      </AnimatePresence>\n      {prefix}\n      {children}\n      {suffix}\n    </button>\n  );\n}\n\nexport default SmoothButton;\nexport { smoothButtonVariants };\n","path":"index.tsx","target":"components/smoothui/smooth-button/index.tsx","type":"registry:ui"}],"name":"smooth-button","registryDependencies":["https://smoothui.dev/r/tokens.json"],"title":"Smooth Button","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"A SocialSelector component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { useState } from \"react\";\n\ntype XIconProps = React.SVGProps<SVGSVGElement> & {\n  className?: string;\n};\n\nexport const XIcon: React.FC<XIconProps> = ({ className, ...props }) => (\n  <svg\n    className={className}\n    viewBox=\"0 0 512 512\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    {...props}\n  >\n    <title>X icon</title>\n    <path d=\"M389.2 48h70.6L305.6 224.2 487 464H345L233.7 318.6 106.5 464H35.8L200.7 275.5 26.8 48H172.4L272.9 180.9 389.2 48zM364.4 421.8h39.1L151.1 88h-42L364.4 421.8z\" />\n  </svg>\n);\n\ntype ThreadsIconProps = React.SVGProps<SVGSVGElement> & {\n  className?: string;\n};\n\nexport const ThreadsIcon: React.FC<ThreadsIconProps> = ({\n  className,\n  ...props\n}) => (\n  <svg\n    className={className}\n    viewBox=\"0 0 448 512\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    {...props}\n  >\n    <title>Threads icon</title>\n    <path d=\"M331.5 235.7c2.2 .9 4.2 1.9 6.3 2.8c29.2 14.1 50.6 35.2 61.8 61.4c15.7 36.5 17.2 95.8-30.3 143.2c-36.2 36.2-80.3 52.5-142.6 53h-.3c-70.2-.5-124.1-24.1-160.4-70.2c-32.3-41-48.9-98.1-49.5-169.6V256v-.2C17 184.3 33.6 127.2 65.9 86.2C102.2 40.1 156.2 16.5 226.4 16h.3c70.3 .5 124.9 24 162.3 69.9c18.4 22.7 32 50 40.6 81.7l-40.4 10.8c-7.1-25.8-17.8-47.8-32.2-65.4c-29.2-35.8-73-54.2-130.5-54.6c-57 .5-100.1 18.8-128.2 54.4C72.1 146.1 58.5 194.3 58 256c.5 61.7 14.1 109.9 40.3 143.3c28 35.6 71.2 53.9 128.2 54.4c51.4-.4 85.4-12.6 113.7-40.9c32.3-32.2 31.7-71.8 21.4-95.9c-6.1-14.2-17.1-26-31.9-34.9c-3.7 26.9-11.8 48.3-24.7 64.8c-17.1 21.8-41.4 33.6-72.7 35.3c-23.6 1.3-46.3-4.4-63.9-16c-20.8-13.8-33-34.8-34.3-59.3c-2.5-48.3 35.7-83 95.2-86.4c21.1-1.2 40.9-.3 59.2 2.8c-2.4-14.8-7.3-26.6-14.6-35.2c-10-11.7-25.6-17.7-46.2-17.8H227c-16.6 0-39 4.6-53.3 26.3l-34.4-23.6c19.2-29.1 50.3-45.1 87.8-45.1h.8c62.6 .4 99.9 39.5 103.7 107.7l-.2 .2zm-156 68.8c1.3 25.1 28.4 36.8 54.6 35.3c25.6-1.4 54.6-11.4 59.5-73.2c-13.2-2.9-27.8-4.4-43.4-4.4c-4.8 0-9.6 .1-14.4 .4c-42.9 2.4-57.2 23.2-56.2 41.8l-.1 .1z\" />\n  </svg>\n);\n\ntype BskyIconProps = React.SVGProps<SVGSVGElement> & {\n  className?: string;\n};\n\nexport const BskyIcon: React.FC<BskyIconProps> = ({ className, ...props }) => (\n  <svg\n    className={className}\n    viewBox=\"0 0 512 512\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    {...props}\n  >\n    <title>Bluesky icon</title>\n    <path d=\"M111.8 62.2C170.2 105.9 233 194.7 256 242.4c23-47.6 85.8-136.4 144.2-180.2c42.1-31.6 110.3-56 110.3 21.8c0 15.5-8.9 130.5-14.1 149.2C478.2 298 412 314.6 353.1 304.5c102.9 17.5 129.1 75.5 72.5 133.5c-107.4 110.2-154.3-27.6-166.3-62.9l0 0c-1.7-4.9-2.6-7.8-3.3-7.8s-1.6 3-3.3 7.8l0 0c-12 35.3-59 173.1-166.3 62.9c-56.5-58-30.4-116 72.5-133.5C100 314.6 33.8 298 15.7 233.1C10.4 214.4 1.5 99.4 1.5 83.9c0-77.8 68.2-53.4 110.3-21.8z\" />\n  </svg>\n);\n\nexport interface Platform {\n  domain: string;\n  icon: React.ReactNode;\n  name: string;\n  url: string;\n}\n\nconst ICON_SIZE = 36;\nconst ICON_GAP = 16;\n\nconst defaultPlatforms: Platform[] = [\n  {\n    domain: \"x.com\",\n    icon: <XIcon className=\"h-5 w-5\" />,\n    name: \"X\",\n    url: \"https://x.com/educalvolpz\",\n  },\n  {\n    domain: \"bsky.app\",\n    icon: <BskyIcon className=\"h-5 w-5\" />,\n    name: \"Bluesky\",\n    url: \"https://bsky.app/profile/educalvolpz.bsky.social\",\n  },\n  {\n    domain: \"threads.net\",\n    icon: <ThreadsIcon className=\"h-5 w-5\" />,\n    name: \"Threads\",\n    url: \"https://threads.net/@educalvolpz\",\n  },\n];\n\nexport interface SocialSelectorProps {\n  className?: string;\n  handle?: string;\n  onChange?: (platform: Platform) => void;\n  platforms?: Platform[];\n  selectedPlatform?: Platform;\n}\n\nexport default function SocialSelector({\n  platforms = defaultPlatforms,\n  handle = \"educalvolpz\",\n  selectedPlatform: controlledSelected,\n  onChange,\n  className = \"\",\n}: SocialSelectorProps) {\n  const [internalSelected, setInternalSelected] = useState<Platform>(\n    platforms[0]\n  );\n  const shouldReduceMotion = useReducedMotion();\n  const selectedPlatform = controlledSelected ?? internalSelected;\n\n  return (\n    <div className={`mx-auto my-4 w-full max-w-2xl text-center ${className}`}>\n      <div className=\"space-y-6\">\n        <div className=\"flex items-center justify-center\">\n          <div className=\"relative flex w-fit items-center justify-center gap-4\">\n            {platforms.map((platform) => (\n              <button\n                aria-label={`Select ${platform.name} platform`}\n                className={`relative z-10 cursor-pointer rounded-full p-2 transition-colors ${\n                  selectedPlatform.name === platform.name\n                    ? \"fill-foreground\"\n                    : \"fill-primary-foreground hover:bg-primary\"\n                }`}\n                key={platform.name}\n                onClick={() => {\n                  if (onChange) {\n                    onChange(platform);\n                  } else {\n                    setInternalSelected(platform);\n                  }\n                }}\n                type=\"button\"\n              >\n                {platform.icon}\n                <span className=\"sr-only\">{platform.name}</span>\n              </button>\n            ))}\n            <motion.div\n              animate={\n                shouldReduceMotion\n                  ? {}\n                  : {\n                      x:\n                        platforms.findIndex(\n                          (p) => p.name === selectedPlatform.name\n                        ) *\n                        (ICON_SIZE + ICON_GAP),\n                    }\n              }\n              className=\"absolute inset-0 z-0 h-9 w-9 rounded-full border bg-background\"\n              initial={false}\n              layoutId={shouldReduceMotion ? undefined : \"background\"}\n              transition={\n                shouldReduceMotion\n                  ? { duration: 0 }\n                  : {\n                      damping: 30,\n                      duration: 0.25,\n                      stiffness: 500,\n                      type: \"spring\" as const,\n                    }\n              }\n            />\n          </div>\n        </div>\n        <p className=\"text-md text-primary-foreground\">\n          Updates on{\" \"}\n          <span className=\"font-medium text-foreground\">\n            <motion.a\n              animate={\n                shouldReduceMotion\n                  ? { opacity: 1 }\n                  : { filter: \"blur(0px)\", opacity: 1, y: 0 }\n              }\n              exit={\n                shouldReduceMotion\n                  ? { opacity: 0, transition: { duration: 0 } }\n                  : { filter: \"blur(5px)\", opacity: 0, y: -10 }\n              }\n              href={selectedPlatform.url}\n              initial={\n                shouldReduceMotion\n                  ? { opacity: 1 }\n                  : { filter: \"blur(5px)\", opacity: 0, y: 10 }\n              }\n              key={selectedPlatform.domain}\n              rel=\"noopener noreferrer\"\n              target=\"_blank\"\n              transition={\n                shouldReduceMotion ? { duration: 0 } : { duration: 0.25 }\n              }\n            >\n              {selectedPlatform.domain}\n            </motion.a>\n          </span>\n          <br />\n          <a\n            className=\"font-medium text-foreground\"\n            href={selectedPlatform.url}\n            rel=\"noopener noreferrer\"\n            target=\"_blank\"\n          >\n            @{handle}\n          </a>\n        </p>\n      </div>\n    </div>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/social-selector/index.tsx","type":"registry:ui"}],"name":"social-selector","registryDependencies":[],"title":"Social Selector","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"A SoftBlurIn text animation component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { motion, useInView, useReducedMotion } from \"motion/react\";\nimport { useRef } from \"react\";\n\nexport interface SoftBlurInProps {\n  children: string;\n  className?: string;\n  /** Delay before the animation starts, in milliseconds. */\n  delay?: number;\n  /** Per-character stagger, in milliseconds. */\n  stagger?: number;\n  /** Animate only once the text scrolls into view. */\n  triggerOnView?: boolean;\n}\n\nconst DURATION_S = 0.9;\nconst MS = 1000;\n// Apple's signature ease-out.\nconst EASE = [0.22, 1, 0.36, 1] as const;\n\n/**\n * SoftBlurIn — per-character fade-in with a gentle blur and upward motion,\n * Apple's signature hero-title reveal. From the animate-text catalog\n * (`soft-blur-in`). Best on hero titles 48px+ over solid backgrounds.\n */\nexport default function SoftBlurIn({\n  children,\n  className = \"\",\n  delay = 0,\n  stagger = 25,\n  triggerOnView = false,\n}: SoftBlurInProps) {\n  const ref = useRef<HTMLSpanElement>(null);\n  const inView = useInView(ref, { once: true });\n  const shouldReduceMotion = useReducedMotion();\n  const play = (!triggerOnView || inView) && !shouldReduceMotion;\n  const characters = Array.from(children);\n\n  return (\n    <span aria-label={children} className={className} ref={ref}>\n      {characters.map((char, index) => (\n        <motion.span\n          animate={play ? { filter: \"blur(0px)\", opacity: 1, y: 0 } : undefined}\n          aria-hidden=\"true\"\n          initial={\n            shouldReduceMotion\n              ? { opacity: 1 }\n              : { filter: \"blur(12px)\", opacity: 0, y: 16 }\n          }\n          // biome-ignore lint/suspicious/noArrayIndexKey: characters have no stable id\n          key={index}\n          style={{ display: \"inline-block\", whiteSpace: \"pre\" }}\n          transition={\n            shouldReduceMotion\n              ? { duration: 0 }\n              : {\n                  delay: delay / MS + (index * stagger) / MS,\n                  duration: DURATION_S,\n                  ease: EASE,\n                }\n          }\n        >\n          {char === \" \" ? \" \" : String(char)}\n        </motion.span>\n      ))}\n    </span>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/soft-blur-in/index.tsx","type":"registry:ui"}],"name":"soft-blur-in","registryDependencies":[],"title":"Soft Blur In","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"A SpringScaleIn text animation component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { motion, useInView, useReducedMotion } from \"motion/react\";\nimport { useRef } from \"react\";\n\n/**\n * SpringScaleIn — per-word scale-in with a soft overshoot, iOS app icon-style pop.\n * From the animate-text catalog (`spring-scale-in`).\n */\nexport interface SpringScaleInProps {\n  children: string;\n  className?: string;\n  /** Delay before the animation starts, in milliseconds. */\n  delay?: number;\n  /** Per-word stagger, in milliseconds. */\n  stagger?: number;\n  /** Animate only once the text scrolls into view. */\n  triggerOnView?: boolean;\n}\n\nconst DURATION_S = 0.36;\nconst MS = 1000;\nconst EASE = [0.34, 1.56, 0.64, 1] as const;\n\nexport default function SpringScaleIn({\n  children,\n  className = \"\",\n  delay = 0,\n  stagger = 95,\n  triggerOnView = false,\n}: SpringScaleInProps) {\n  const ref = useRef<HTMLSpanElement>(null);\n  const inView = useInView(ref, { once: true });\n  const shouldReduceMotion = useReducedMotion();\n  const play = (!triggerOnView || inView) && !shouldReduceMotion;\n  const words = children.split(\" \");\n\n  return (\n    <span aria-label={children} className={className} ref={ref}>\n      {words.map((word, index) => (\n        // biome-ignore lint/suspicious/noArrayIndexKey: words have no stable id\n        <span key={index}>\n          <motion.span\n            animate={play ? { opacity: 1, scale: 1 } : undefined}\n            aria-hidden=\"true\"\n            initial={\n              shouldReduceMotion ? { opacity: 1 } : { opacity: 0, scale: 0.7 }\n            }\n            style={{ display: \"inline-block\", whiteSpace: \"pre\" }}\n            transition={\n              shouldReduceMotion\n                ? { duration: 0 }\n                : {\n                    delay: delay / MS + (index * stagger) / MS,\n                    duration: DURATION_S,\n                    ease: EASE,\n                  }\n            }\n          >\n            {word}\n          </motion.span>\n          {index < words.length - 1 && (\n            <span\n              aria-hidden=\"true\"\n              style={{ display: \"inline-block\", whiteSpace: \"pre\" }}\n            >\n              {\" \"}\n            </span>\n          )}\n        </span>\n      ))}\n    </span>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/spring-scale-in/index.tsx","type":"registry:ui"}],"name":"spring-scale-in","registryDependencies":[],"title":"Spring Scale In","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"A StaggerFromCenter text animation component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { motion, useInView, useReducedMotion } from \"motion/react\";\nimport { useRef } from \"react\";\n\nexport interface StaggerFromCenterProps {\n  children: string;\n  className?: string;\n  /** Delay before the animation starts, in milliseconds. */\n  delay?: number;\n  /** Per-step stagger between characters, in milliseconds. */\n  stagger?: number;\n  /** Animate only once the text scrolls into view. */\n  triggerOnView?: boolean;\n}\n\nconst DURATION_S = 0.62;\nconst MS = 1000;\n// Apple-ish ease-out for center-out reveal.\nconst EASE = [0.22, 1, 0.36, 1] as const;\n\n/**\n * StaggerFromCenter — characters reveal from the center outward to\n * emphasize the keyword core. Delay is computed by distance from center,\n * not linear index. From the animate-text catalog (`stagger-from-center`).\n */\nexport default function StaggerFromCenter({\n  children,\n  className = \"\",\n  delay = 0,\n  stagger = 22,\n  triggerOnView = false,\n}: StaggerFromCenterProps) {\n  const ref = useRef<HTMLSpanElement>(null);\n  const inView = useInView(ref, { once: true });\n  const shouldReduceMotion = useReducedMotion();\n  const play = (!triggerOnView || inView) && !shouldReduceMotion;\n  const characters = Array.from(children);\n  const center = (characters.length - 1) / 2;\n\n  return (\n    <span aria-label={children} className={className} ref={ref}>\n      {characters.map((char, index) => {\n        const distance = Math.abs(index - center);\n        return (\n          <motion.span\n            animate={\n              play ? { filter: \"blur(0px)\", opacity: 1, y: 0 } : undefined\n            }\n            aria-hidden=\"true\"\n            initial={\n              shouldReduceMotion\n                ? { opacity: 1 }\n                : { filter: \"blur(3px)\", opacity: 0, y: 12 }\n            }\n            // biome-ignore lint/suspicious/noArrayIndexKey: characters have no stable id\n            key={index}\n            style={{ display: \"inline-block\", whiteSpace: \"pre\" }}\n            transition={\n              shouldReduceMotion\n                ? { duration: 0 }\n                : {\n                    delay: delay / MS + (distance * stagger) / MS,\n                    duration: DURATION_S,\n                    ease: EASE,\n                  }\n            }\n          >\n            {char === \" \" ? \" \" : String(char)}\n          </motion.span>\n        );\n      })}\n    </span>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/stagger-from-center/index.tsx","type":"registry:ui"}],"name":"stagger-from-center","registryDependencies":[],"title":"Stagger From Center","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"A StaggerFromEdges text animation component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { motion, useInView, useReducedMotion } from \"motion/react\";\nimport { useRef } from \"react\";\n\nexport interface StaggerFromEdgesProps {\n  children: string;\n  className?: string;\n  /** Delay before the animation starts, in milliseconds. */\n  delay?: number;\n  /** Per-step stagger between characters, in milliseconds. */\n  stagger?: number;\n  /** Animate only once the text scrolls into view. */\n  triggerOnView?: boolean;\n}\n\nconst DURATION_S = 0.62;\nconst MS = 1000;\n// Apple-ish ease-out for edges-in reveal.\nconst EASE = [0.22, 1, 0.36, 1] as const;\n\n/**\n * StaggerFromEdges — characters start from both edges and converge toward\n * the center. Delay is computed by distance from the nearest edge, not\n * linear index. From the animate-text catalog (`stagger-from-edges`).\n */\nexport default function StaggerFromEdges({\n  children,\n  className = \"\",\n  delay = 0,\n  stagger = 22,\n  triggerOnView = false,\n}: StaggerFromEdgesProps) {\n  const ref = useRef<HTMLSpanElement>(null);\n  const inView = useInView(ref, { once: true });\n  const shouldReduceMotion = useReducedMotion();\n  const play = (!triggerOnView || inView) && !shouldReduceMotion;\n  const characters = Array.from(children);\n  const lastIndex = characters.length - 1;\n\n  return (\n    <span aria-label={children} className={className} ref={ref}>\n      {characters.map((char, index) => {\n        // Distance from the nearest edge (0 = edge character, increases toward center)\n        const distanceFromEdge = Math.min(index, lastIndex - index);\n        return (\n          <motion.span\n            animate={\n              play ? { filter: \"blur(0px)\", opacity: 1, y: 0 } : undefined\n            }\n            aria-hidden=\"true\"\n            initial={\n              shouldReduceMotion\n                ? { opacity: 1 }\n                : { filter: \"blur(3px)\", opacity: 0, y: 12 }\n            }\n            // biome-ignore lint/suspicious/noArrayIndexKey: characters have no stable id\n            key={index}\n            style={{ display: \"inline-block\", whiteSpace: \"pre\" }}\n            transition={\n              shouldReduceMotion\n                ? { duration: 0 }\n                : {\n                    delay: delay / MS + (distanceFromEdge * stagger) / MS,\n                    duration: DURATION_S,\n                    ease: EASE,\n                  }\n            }\n          >\n            {char === \" \" ? \" \" : String(char)}\n          </motion.span>\n        );\n      })}\n    </span>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/stagger-from-edges/index.tsx","type":"registry:ui"}],"name":"stagger-from-edges","registryDependencies":[],"title":"Stagger From Edges","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":[],"description":"A SwitchboardCard component with light grid illustration for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { useEffect, useMemo, useState } from \"react\";\n\nexport type LightState = \"off\" | \"medium\" | \"high\";\n\nexport interface SwitchboardCardProps {\n  className?: string;\n  columns?: number;\n  gridPattern?: number[] | number[][]; // Direct grid pattern: flat array (columns*rows) or 2D array [rows][columns] where 0=off, 1=high\n  href?: string;\n  onButtonClick?: () => void;\n  randomLights?: boolean; // If true, randomly activate some lights\n  rows?: number;\n  showButton?: boolean;\n  subtitle: string;\n  title: string;\n  transitionDuration?: number; // in milliseconds (default: 200ms for smooth light transitions)\n  variant?: \"default\" | \"next\";\n}\n\nfunction generateRandomPattern(\n  totalLights: number,\n  activeRatio = 0.15\n): number[] {\n  const activeCount = Math.floor(totalLights * activeRatio);\n  const pattern: number[] = [];\n  const used = new Set<number>();\n\n  while (pattern.length < activeCount) {\n    const index = Math.floor(Math.random() * totalLights);\n    if (!used.has(index)) {\n      used.add(index);\n      pattern.push(index);\n    }\n  }\n\n  return pattern.sort((a, b) => a - b);\n}\n\nfunction parse2DGridPattern(\n  grid2D: number[][],\n  rows: number,\n  columns: number,\n  totalLights: number\n): number[] {\n  const pattern: number[] = [];\n  for (let row = 0; row < Math.min(grid2D.length, rows); row++) {\n    const rowData = grid2D[row];\n    if (Array.isArray(rowData)) {\n      for (let col = 0; col < Math.min(rowData.length, columns); col++) {\n        if (rowData[col] === 1) {\n          const index = col + row * columns;\n          if (index >= 0 && index < totalLights) {\n            pattern.push(index);\n          }\n        }\n      }\n    }\n  }\n  return pattern;\n}\n\nfunction parseFlatGridPattern(\n  flatPattern: number[],\n  totalLights: number\n): number[] {\n  const pattern: number[] = [];\n  for (let i = 0; i < Math.min(flatPattern.length, totalLights); i++) {\n    if (flatPattern[i] === 1) {\n      pattern.push(i);\n    }\n  }\n  return pattern;\n}\n\nexport default function SwitchboardCard({\n  title,\n  subtitle,\n  columns = 18,\n  rows = 5,\n  gridPattern,\n  randomLights = false,\n  transitionDuration = 200,\n  className,\n  variant = \"default\",\n  showButton: _showButton = true,\n  href,\n  onButtonClick,\n}: SwitchboardCardProps) {\n  const totalLights = columns * rows;\n  const isRandomLightsMode = randomLights && !gridPattern;\n\n  // Convert gridPattern to light indices\n  // Priority: gridPattern > randomLights\n  const basePattern = useMemo<number[]>(() => {\n    if (gridPattern) {\n      // Handle 2D array [rows][columns]\n      const is2DArray =\n        Array.isArray(gridPattern[0]) && typeof gridPattern[0] !== \"number\";\n      if (is2DArray) {\n        return parse2DGridPattern(\n          gridPattern as number[][],\n          rows,\n          columns,\n          totalLights\n        );\n      }\n      // Handle flat array [columns*rows]\n      return parseFlatGridPattern(gridPattern as number[], totalLights);\n    }\n    if (randomLights) {\n      return generateRandomPattern(totalLights);\n    }\n    return [];\n  }, [gridPattern, randomLights, columns, rows, totalLights]);\n\n  // Animated light states\n  // For random lights: all start off, will animate randomly through medium → high → medium → off\n  // For word/pattern: pattern lights start high (stay high), rest off\n  const [lightStates, setLightStates] = useState<LightState[]>(() => {\n    const states: LightState[] = new Array(totalLights).fill(\"off\");\n    if (!isRandomLightsMode) {\n      // For word/pattern, set pattern lights to high\n      for (const index of basePattern) {\n        if (index >= 0 && index < totalLights) {\n          states[index] = \"high\";\n        }\n      }\n    }\n    // For random lights, all start off (will animate)\n    return states;\n  });\n\n  // Animate lights - only for random lights mode (word/pattern stays high)\n  useEffect(() => {\n    // Word/pattern mode: no animation, lights stay high\n    if (!isRandomLightsMode) {\n      return;\n    }\n\n    const interval = setInterval(() => {\n      setLightStates((prev) => {\n        const next = [...prev];\n        // For random lights: animate all lights randomly through off → high (via medium transition)\n        // Randomly select a subset of lights to animate each tick\n        const lightsToAnimate = Math.floor(totalLights * 0.2); // Animate ~20% of all lights per tick\n        const allIndices = Array.from({ length: totalLights }, (_, i) => i);\n        const shuffled = allIndices.sort(() => Math.random() - 0.5);\n\n        for (let i = 0; i < Math.min(lightsToAnimate, shuffled.length); i++) {\n          const index = shuffled[i];\n          if (index >= 0 && index < totalLights) {\n            const current = prev[index];\n            // State transition logic for random lights: off → high (with medium as intermediate transition state)\n            // Medium state is only used as a transition, not a final state\n            if (current === \"off\") {\n              // Start transition to high by going to medium first\n              next[index] = \"medium\";\n            } else if (current === \"medium\") {\n              // Continue transition: medium → high\n              next[index] = \"high\";\n            } else if (current === \"high\") {\n              // Turn off directly (no medium transition when turning off)\n              next[index] = \"off\";\n            }\n          }\n        }\n        return next;\n      });\n    }, 200); // 0.2s interval to match transition duration\n\n    return () => clearInterval(interval);\n  }, [isRandomLightsMode, totalLights]);\n\n  const isNextVariant = variant === \"next\";\n\n  const CardContent = (\n    <div\n      className={cn(\n        \"relative flex h-[380px] w-full flex-col overflow-hidden rounded-xl border p-6 transition-[border-color,background-color] duration-150\",\n        isNextVariant\n          ? \"border-0 dark:border-0 dark:shadow-[inset_0_0_6px_rgba(255,255,255,0.1)]\"\n          : \"border-border bg-background hover:border-foreground/20\",\n        className\n      )}\n      data-variant={isNextVariant ? \"next\" : undefined}\n      style={\n        isNextVariant\n          ? ({\n              background:\n                \"linear-gradient(110deg, oklch(0.65 0 0) 0.06%, oklch(0.54 0 0) 100%)\",\n            } as React.CSSProperties)\n          : undefined\n      }\n    >\n      {/* Illustration/Switchboard */}\n      <div\n        className=\"flex h-full w-full items-center justify-center\"\n        data-illustration=\"true\"\n      >\n        <div\n          className=\"grid h-full w-full\"\n          style={{\n            gap: `min(1px, calc(100% / ${columns} / 10))`,\n            gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))`,\n            gridTemplateRows: `repeat(${rows}, minmax(0, 1fr))`,\n            maxHeight: \"81px\",\n            transitionDuration: `${transitionDuration}ms`,\n          }}\n        >\n          {lightStates.map((state, index) => (\n            <LightBulb\n              // biome-ignore lint/suspicious/noArrayIndexKey: Light grid position is stable and never reorders\n              key={`light-${index}`}\n              state={state}\n              transitionDuration={transitionDuration}\n            />\n          ))}\n        </div>\n      </div>\n\n      {/* Title */}\n      <div className=\"mt-4 flex flex-row items-center justify-start gap-2 text-left\">\n        <h3\n          className={cn(\n            \"text-left font-semibold leading-8 tracking-[-0.04em]\",\n            isNextVariant ? \"mt-4 text-2xl\" : \"text-foreground text-xl\"\n          )}\n          data-title=\"true\"\n          style={\n            isNextVariant\n              ? ({ color: \"oklch(0.95 0 0)\" } as React.CSSProperties)\n              : undefined\n          }\n        >\n          {title}\n        </h3>\n      </div>\n\n      {/* Subtitle */}\n      <p\n        className={cn(\n          \"text-left text-sm leading-relaxed tracking-[-0.01em]\",\n          isNextVariant\n            ? \"mt-1 max-w-[80%] text-base\"\n            : \"mt-1 text-foreground/70\"\n        )}\n        data-subtitle=\"true\"\n        style={\n          isNextVariant\n            ? ({ color: \"oklch(0.9 0 0)\" } as React.CSSProperties)\n            : undefined\n        }\n      >\n        {subtitle}\n      </p>\n    </div>\n  );\n\n  if (href) {\n    return (\n      <a\n        aria-label={title}\n        className=\"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\"\n        href={href}\n      >\n        {CardContent}\n      </a>\n    );\n  }\n\n  if (onButtonClick) {\n    return (\n      <button\n        aria-label={title}\n        className=\"cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\"\n        onClick={onButtonClick}\n        type=\"button\"\n      >\n        {CardContent}\n      </button>\n    );\n  }\n\n  return CardContent;\n}\n\nfunction LightBulb({\n  state,\n  transitionDuration,\n}: {\n  state: LightState;\n  transitionDuration: number;\n}) {\n  const transitionStyle = {\n    transition: `opacity ${transitionDuration}ms ease-out, transform ${transitionDuration}ms ease-out`,\n  };\n\n  return (\n    <div\n      className={cn(\n        \"relative rounded-full\",\n        state === \"off\" && \"bg-foreground/50\",\n        state === \"high\" ? \"h-[2px] w-[2px]\" : \"h-px w-px\"\n      )}\n      data-state={state}\n      style={{\n        ...transitionStyle,\n        transform: state === \"off\" ? \"unset\" : \"scale(1)\",\n      }}\n    >\n      {/* Glow effect for medium state (::before equivalent) */}\n      {state === \"medium\" && (\n        <div\n          className=\"absolute inset-0 rounded-full\"\n          style={{\n            ...transitionStyle,\n            backgroundColor: \"var(--color-brand)\",\n            boxShadow:\n              \"0 0 2px 1px color-mix(in oklch, var(--color-brand), transparent 60%), 0 0 4px 1.5px color-mix(in oklch, var(--color-brand), transparent 75%)\",\n            opacity: 1,\n          }}\n        />\n      )}\n\n      {/* Glow effect for high state (::after equivalent) */}\n      {state === \"high\" && (\n        <div\n          className=\"absolute inset-0 rounded-full\"\n          style={{\n            ...transitionStyle,\n            backgroundColor: \"var(--color-brand)\",\n            boxShadow:\n              \"0 0 2px 1px color-mix(in oklch, var(--color-brand), transparent 40%), 0 0 4px 1.5px color-mix(in oklch, var(--color-brand), transparent 65%), 0 0 6px 2px color-mix(in oklch, var(--color-brand), transparent 80%)\",\n            opacity: 1,\n          }}\n        />\n      )}\n    </div>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/switchboard-card/index.tsx","type":"registry:ui"}],"name":"switchboard-card","registryDependencies":["https://smoothui.dev/r/tokens.json"],"title":"Switchboard Card","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"A TopDownLetters text animation component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { motion, useInView, useReducedMotion } from \"motion/react\";\nimport { useRef } from \"react\";\n\nexport interface TopDownLettersProps {\n  children: string;\n  className?: string;\n  /** Delay before the animation starts, in milliseconds. */\n  delay?: number;\n  /** Per-character stagger, in milliseconds. */\n  stagger?: number;\n  /** Animate only once the text scrolls into view. */\n  triggerOnView?: boolean;\n}\n\nconst DURATION_S = 0.4;\nconst MS = 1000;\n// Pronounced ease-out for a tall, staged drop from above.\nconst EASE = [0.18, 1, 0.32, 1] as const;\n\n/**\n * TopDownLetters — letters descend from above in a pronounced staircase,\n * one symbol at a time, with zero blur. The top-down counterpart to\n * bottom-up-letters. From the animate-text catalog (`top-down-letters`).\n */\nexport default function TopDownLetters({\n  children,\n  className = \"\",\n  delay = 0,\n  stagger = 88,\n  triggerOnView = false,\n}: TopDownLettersProps) {\n  const ref = useRef<HTMLSpanElement>(null);\n  const inView = useInView(ref, { once: true });\n  const shouldReduceMotion = useReducedMotion();\n  const play = (!triggerOnView || inView) && !shouldReduceMotion;\n  const characters = Array.from(children);\n\n  return (\n    <span aria-label={children} className={className} ref={ref}>\n      {characters.map((char, index) => (\n        <motion.span\n          animate={play ? { opacity: 1, y: 0 } : undefined}\n          aria-hidden=\"true\"\n          initial={shouldReduceMotion ? { opacity: 1 } : { opacity: 0, y: -46 }}\n          // biome-ignore lint/suspicious/noArrayIndexKey: characters have no stable id\n          key={index}\n          style={{ display: \"inline-block\", whiteSpace: \"pre\" }}\n          transition={\n            shouldReduceMotion\n              ? { duration: 0 }\n              : {\n                  delay: delay / MS + (index * stagger) / MS,\n                  duration: DURATION_S,\n                  ease: EASE,\n                }\n          }\n        >\n          {char === \" \" ? \" \" : String(char)}\n        </motion.span>\n      ))}\n    </span>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/top-down-letters/index.tsx","type":"registry:ui"}],"name":"top-down-letters","registryDependencies":[],"title":"Top Down Letters","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["react-tweet"],"description":"A beautiful tweet card component for displaying Twitter/X posts.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { type TweetProps, useTweet } from \"react-tweet\";\n\nimport { SmoothTweet, TweetNotFound, TweetSkeleton } from \"./index\";\n\nexport type ClientTweetCardProps = TweetProps & {\n  className?: string;\n  userInfoPosition?: \"top\" | \"bottom\";\n  avatarRounded?: string;\n};\n\nexport const ClientTweetCard = ({\n  id,\n  apiUrl,\n  fallback = <TweetSkeleton />,\n  components,\n  fetchOptions,\n  onError,\n  ...props\n}: ClientTweetCardProps) => {\n  const { data, error, isLoading } = useTweet(id, apiUrl, fetchOptions);\n\n  if (isLoading) {\n    return fallback;\n  }\n  if (error || !data) {\n    const NotFound = components?.TweetNotFound || TweetNotFound;\n    return <NotFound error={onError ? onError(error) : error} />;\n  }\n\n  return <SmoothTweet tweet={data} {...props} />;\n};\n","path":"client.tsx","target":"components/smoothui/tweet-card/client.tsx","type":"registry:ui"},{"content":"/* eslint-disable @next/next/no-img-element */\n\nimport { cn } from \"@/lib/utils\";\nimport { Suspense } from \"react\";\nimport { type EnrichedTweet, enrichTweet, type TweetProps } from \"react-tweet\";\nimport { getTweet, type Tweet } from \"react-tweet/api\";\n\ninterface IconProps {\n  className?: string;\n  [key: string]: unknown;\n}\n\nconst ExternalLink = ({ className, ...props }: IconProps) => (\n  <svg\n    aria-hidden=\"true\"\n    className={className}\n    fill=\"none\"\n    height=\"16\"\n    stroke=\"currentColor\"\n    strokeLinecap=\"round\"\n    strokeLinejoin=\"round\"\n    strokeWidth=\"2\"\n    viewBox=\"0 0 24 24\"\n    width=\"16\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    {...props}\n  >\n    <path d=\"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\" />\n    <polyline points=\"15 3 21 3 21 9\" />\n    <line x1=\"10\" x2=\"21\" y1=\"14\" y2=\"3\" />\n  </svg>\n);\n\nexport const truncate = (str: string | null, length: number) => {\n  if (!str || str.length <= length) {\n    return str;\n  }\n  return `${str.slice(0, length - 3)}...`;\n};\n\nconst Skeleton = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div className={cn(\"rounded-md bg-muted\", className)} {...props} />\n);\n\nexport const TweetSkeleton = ({\n  className,\n  ...props\n}: {\n  className?: string;\n  [key: string]: unknown;\n}) => (\n  <div\n    className={cn(\n      \"flex size-full max-h-max min-w-72 flex-col gap-2 rounded-xl border p-4\",\n      className\n    )}\n    {...props}\n  >\n    <div className=\"flex flex-row gap-2\">\n      <Skeleton className=\"size-10 shrink-0 rounded-full\" />\n      <Skeleton className=\"h-10 w-full\" />\n    </div>\n    <Skeleton className=\"h-20 w-full\" />\n  </div>\n);\n\nexport const TweetNotFound = ({\n  className,\n  ...props\n}: {\n  className?: string;\n  [key: string]: unknown;\n}) => (\n  <div\n    className={cn(\n      \"flex size-full flex-col items-center justify-center gap-2 rounded-lg border p-4\",\n      className\n    )}\n    {...props}\n  >\n    <h3>Tweet not found</h3>\n  </div>\n);\n\nexport const TweetHeader = ({\n  tweet,\n  avatarRounded = \"rounded\",\n}: {\n  tweet: EnrichedTweet;\n  avatarRounded?: string;\n}) => (\n  <div className=\"flex items-center gap-2\">\n    <a href={tweet.user.url} rel=\"noreferrer\" target=\"_blank\">\n      <img\n        alt={tweet.user.screen_name}\n        className={cn(\"size-10 object-cover object-center\", avatarRounded)}\n        draggable={false}\n        height={40}\n        loading=\"eager\"\n        src={tweet.user.profile_image_url_https}\n        title={`Profile picture of ${tweet.user.name}`}\n        width={40}\n      />\n    </a>\n    <div>\n      <p className=\"font-semibold text-foreground text-sm tracking-tighter 2xl:text-base\">\n        {tweet.user.name}\n      </p>\n      <p className=\"text-foreground/60 text-xs 2xl:text-sm\">\n        @{truncate(tweet.user.screen_name, 16)}\n      </p>\n    </div>\n  </div>\n);\n\nexport const TweetBody = ({ tweet }: { tweet: EnrichedTweet }) => (\n  <blockquote className=\"flex-1\">\n    <p className=\"text-balance text-foreground text-sm tracking-tight 2xl:text-base\">\n      {tweet.entities.map((entity, idx) => {\n        switch (entity.type) {\n          case \"url\":\n          case \"symbol\":\n          case \"hashtag\":\n          case \"mention\":\n            return (\n              <a\n                className=\"ease text-foreground transition-colors duration-200 hover:text-foreground/80\"\n                href={entity.href}\n                key={`${entity.type}-${idx}-${entity.text.slice(0, 10)}`}\n                rel=\"noopener noreferrer\"\n                target=\"_blank\"\n              >\n                <span>{entity.text}</span>\n              </a>\n            );\n          case \"text\":\n            return (\n              <span\n                className=\"text-foreground\"\n                // biome-ignore lint/suspicious/noArrayIndexKey: Text entities from tweet API have no unique IDs\n                key={`text-${idx}`}\n              >\n                {entity.text}\n              </span>\n            );\n          default:\n            return null;\n        }\n      })}\n    </p>\n  </blockquote>\n);\n\nexport const TweetMedia = ({ tweet }: { tweet: EnrichedTweet }) => {\n  const hasVideo =\n    tweet.video &&\n    Array.isArray(tweet.video.variants) &&\n    tweet.video.variants.length > 0;\n  const hasPhotos = tweet.photos && tweet.photos.length > 0;\n  const hasCardThumbnail =\n    // @ts-expect-error package doesn't have type definitions\n    tweet?.card?.binding_values?.thumbnail_image_large?.image_value.url;\n\n  if (!(hasVideo || hasPhotos || hasCardThumbnail)) {\n    return null;\n  }\n\n  return (\n    <div className=\"flex w-full flex-col gap-2\">\n      {hasVideo && tweet.video ? (\n        <video\n          autoPlay\n          className=\"w-full rounded-xl border shadow-sm\"\n          loop\n          muted\n          playsInline\n          poster={tweet.video.poster}\n        >\n          <source src={tweet.video.variants[0].src} type=\"video/mp4\" />\n          Your browser does not support the video tag.\n        </video>\n      ) : null}\n      {hasPhotos && tweet.photos\n        ? (() => {\n            const { photos } = tweet;\n            return (\n              <div className=\"relative grid w-full gap-2\">\n                {photos.length === 1 && (\n                  <img\n                    alt={tweet.text}\n                    className=\"w-full rounded-xl border object-cover shadow-sm\"\n                    draggable={false}\n                    height={photos[0].height}\n                    key={photos[0].url}\n                    src={photos[0].url}\n                    title={`Photo by ${tweet.user.name}`}\n                    width={photos[0].width}\n                  />\n                )}\n                {photos.length === 2 && (\n                  <div className=\"grid grid-cols-2 gap-2\">\n                    {photos.map((photo) => (\n                      <img\n                        alt={tweet.text}\n                        className=\"w-full rounded-xl border object-cover shadow-sm\"\n                        draggable={false}\n                        height={photo.height}\n                        key={photo.url}\n                        src={photo.url}\n                        title={`Photo by ${tweet.user.name}`}\n                        width={photo.width}\n                      />\n                    ))}\n                  </div>\n                )}\n                {photos.length >= 3 && (\n                  <div className=\"grid grid-cols-2 gap-2\">\n                    {photos.slice(0, 4).map((photo, index) => (\n                      <img\n                        alt={tweet.text}\n                        className={cn(\n                          \"w-full rounded-xl border object-cover shadow-sm\",\n                          index === 0 && photos.length > 3 && \"row-span-2\"\n                        )}\n                        draggable={false}\n                        height={photo.height}\n                        key={photo.url}\n                        src={photo.url}\n                        title={`Photo by ${tweet.user.name}`}\n                        width={photo.width}\n                      />\n                    ))}\n                  </div>\n                )}\n              </div>\n            );\n          })()\n        : null}\n      {!(hasVideo || hasPhotos) && hasCardThumbnail && (\n        <img\n          alt={tweet.text}\n          className=\"w-full rounded-xl border object-cover shadow-sm\"\n          draggable={false}\n          src={\n            // @ts-expect-error package doesn't have type definitions\n            tweet.card.binding_values.thumbnail_image_large.image_value.url\n          }\n        />\n      )}\n    </div>\n  );\n};\n\nexport const SmoothTweet = ({\n  tweet,\n  className,\n  userInfoPosition = \"bottom\",\n  avatarRounded = \"rounded\",\n  ...props\n}: {\n  tweet: Tweet;\n  className?: string;\n  userInfoPosition?: \"top\" | \"bottom\";\n  avatarRounded?: string;\n}) => {\n  // react-tweet's enrichTweet iterates over each entity array without\n  // guarding for undefined. The syndication API can omit arrays (e.g.\n  // symbols/user_mentions) for some tweets, which would throw\n  // \"entities is not iterable\" and crash the whole tree. Normalize first.\n  const safeTweet: Tweet = {\n    ...tweet,\n    entities: {\n      hashtags: tweet.entities?.hashtags ?? [],\n      symbols: tweet.entities?.symbols ?? [],\n      urls: tweet.entities?.urls ?? [],\n      user_mentions: tweet.entities?.user_mentions ?? [],\n      ...(tweet.entities?.media ? { media: tweet.entities.media } : {}),\n    },\n  };\n  const enrichedTweet = enrichTweet(safeTweet);\n  const userInfo = (\n    <TweetHeader avatarRounded={avatarRounded} tweet={enrichedTweet} />\n  );\n  const content = (\n    <div className=\"flex w-full flex-col gap-4\">\n      <TweetMedia tweet={enrichedTweet} />\n      {userInfoPosition === \"bottom\" && userInfo}\n    </div>\n  );\n\n  return (\n    <article\n      className={cn(\n        \"group !p-6 relative flex flex-col items-start justify-between gap-6 overflow-hidden rounded-xl border bg-background lg:gap-8\",\n        className\n      )}\n      data-tweet\n      {...props}\n    >\n      {userInfoPosition === \"top\" && userInfo}\n      <TweetBody tweet={enrichedTweet} />\n      {content}\n      <button\n        aria-label=\"Open tweet in new tab\"\n        className=\"absolute right-6 bottom-6 z-10 flex h-8 w-8 cursor-pointer items-center justify-center rounded-full border bg-primary text-foreground opacity-0 backdrop-blur-sm transition-all duration-200 ease-out group-hover:opacity-100 motion-safe:hover:scale-110\"\n        onClick={() =>\n          window.open(enrichedTweet.url, \"_blank\", \"noopener,noreferrer\")\n        }\n        type=\"button\"\n      >\n        <ExternalLink className=\"h-4 w-4\" />\n      </button>\n    </article>\n  );\n};\n\n/**\n * TweetCard (Server Side Only)\n */\nexport type TweetCardProps = TweetProps & {\n  className?: string;\n  userInfoPosition?: \"top\" | \"bottom\";\n  avatarRounded?: string;\n};\n\nexport const TweetCard = async ({\n  id,\n  components,\n  fallback = <TweetSkeleton />,\n  onError,\n  ...props\n}: TweetCardProps) => {\n  const tweet = id\n    ? await getTweet(id).catch((err) => {\n        if (onError) {\n          onError(err);\n        } else {\n          console.error(err);\n        }\n      })\n    : undefined;\n\n  if (!tweet) {\n    const NotFound = components?.TweetNotFound || TweetNotFound;\n    return <NotFound {...props} />;\n  }\n\n  return (\n    <Suspense fallback={fallback}>\n      <SmoothTweet tweet={tweet} {...props} />\n    </Suspense>\n  );\n};\n\nexport { ClientTweetCard } from \"./client\";\nexport default TweetCard;\n","path":"index.tsx","target":"components/smoothui/tweet-card/index.tsx","type":"registry:ui"}],"name":"tweet-card","registryDependencies":[],"title":"Tweet Card","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":[],"description":"A TypewriterText component for SmoothUI.","devDependencies":[],"files":[{"content":"import type React from \"react\";\nimport { useEffect, useRef, useState } from \"react\";\n\nfunction useReducedMotion() {\n  const [shouldReduceMotion, setShouldReduceMotion] = useState(false);\n\n  useEffect(() => {\n    const mediaQuery = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    setShouldReduceMotion(mediaQuery.matches);\n\n    const handleChange = (e: MediaQueryListEvent) => {\n      setShouldReduceMotion(e.matches);\n    };\n\n    mediaQuery.addEventListener(\"change\", handleChange);\n    return () => mediaQuery.removeEventListener(\"change\", handleChange);\n  }, []);\n\n  return shouldReduceMotion;\n}\n\nexport interface TypewriterTextProps {\n  children: string;\n  className?: string;\n  loop?: boolean;\n  speed?: number;\n}\n\nconst LOOP_RESTART_DELAY_MS = 1000;\n\nconst TypewriterText: React.FC<TypewriterTextProps> = ({\n  children,\n  speed = 50,\n  loop = false,\n  className = \"\",\n}) => {\n  const [displayed, setDisplayed] = useState(\"\");\n  const index = useRef(0);\n  const timeout = useRef<NodeJS.Timeout | null>(null);\n  const shouldReduceMotion = useReducedMotion();\n\n  useEffect(() => {\n    if (shouldReduceMotion) {\n      // Show full text immediately when reduced motion is enabled\n      setDisplayed(children);\n      return;\n    }\n\n    setDisplayed(\"\");\n    index.current = 0;\n    function type() {\n      setDisplayed(children.slice(0, index.current + 1));\n      if (index.current < children.length - 1) {\n        index.current++;\n        timeout.current = setTimeout(type, speed);\n      } else if (loop) {\n        timeout.current = setTimeout(() => {\n          setDisplayed(\"\");\n          index.current = 0;\n          type();\n        }, LOOP_RESTART_DELAY_MS);\n      }\n    }\n    type();\n    return () => {\n      if (timeout.current) {\n        clearTimeout(timeout.current);\n      }\n    };\n  }, [children, speed, loop, shouldReduceMotion]);\n\n  return <span className={className}>{displayed}</span>;\n};\n\nexport default TypewriterText;\n","path":"index.tsx","target":"components/smoothui/typewriter-text/index.tsx","type":"registry:ui"}],"name":"typewriter-text","registryDependencies":[],"title":"Typewriter Text","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion","lucide-react","@radix-ui/react-popover"],"description":"A UserAccountAvatar component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport {\n  Content as PopoverContent,\n  Portal as PopoverPortal,\n  Root as PopoverRoot,\n  Trigger as PopoverTrigger,\n} from \"@radix-ui/react-popover\";\nimport { Eye, Package, User } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useState } from \"react\";\n\nexport interface UserData {\n  avatar: string;\n  email: string;\n  name: string;\n}\n\nexport interface Order {\n  date: string;\n  id: string;\n  progress: number;\n  status: \"processing\" | \"shipped\" | \"delivered\";\n}\n\nexport interface UserAccountAvatarProps {\n  className?: string;\n  onOrderView?: (orderId: string) => void;\n  onProfileSave?: (user: UserData) => void;\n  orders?: Order[];\n  user: UserData;\n}\n\nconst mockOrders: Order[] = [\n  { date: \"2023-03-15\", id: \"ORD001\", progress: 100, status: \"delivered\" },\n  { date: \"2023-03-20\", id: \"ORD002\", progress: 66, status: \"shipped\" },\n];\n\nexport default function UserAccountAvatar({\n  user,\n  orders = mockOrders,\n  onProfileSave,\n  onOrderView,\n  className = \"\",\n}: UserAccountAvatarProps) {\n  const [activeSection, setActiveSection] = useState<string | null>(null);\n  const [userData, setUserData] = useState<UserData>(user);\n  const shouldReduceMotion = useReducedMotion();\n\n  const handleSectionClick = (section: string) => {\n    setActiveSection(activeSection === section ? null : section);\n  };\n\n  const handleProfileSave = (e: React.FormEvent<HTMLFormElement>) => {\n    e.preventDefault();\n    const formData = new FormData(e.currentTarget);\n    const updatedUser = {\n      ...userData,\n      email: formData.get(\"email\") as string,\n      name: formData.get(\"name\") as string,\n    };\n    setUserData(updatedUser);\n    if (onProfileSave) {\n      onProfileSave(updatedUser);\n    }\n    setActiveSection(null);\n  };\n\n  const renderEditProfile = () => (\n    <form className=\"flex flex-col gap-3 p-4\" onSubmit={handleProfileSave}>\n      <div className=\"flex flex-col gap-1.5\">\n        <label\n          className=\"font-medium text-muted-foreground text-xs\"\n          htmlFor=\"name\"\n        >\n          Name\n        </label>\n        <input\n          className=\"rounded-md border border-border bg-background px-3 py-2 text-foreground text-sm outline-none transition-colors placeholder:text-muted-foreground focus:border-primary focus:ring-1 focus:ring-primary\"\n          defaultValue={userData.name}\n          id=\"name\"\n          name=\"name\"\n          placeholder=\"Enter your name\"\n          type=\"text\"\n        />\n      </div>\n      <div className=\"flex flex-col gap-1.5\">\n        <label\n          className=\"font-medium text-muted-foreground text-xs\"\n          htmlFor=\"email\"\n        >\n          Email\n        </label>\n        <input\n          className=\"rounded-md border border-border bg-background px-3 py-2 text-foreground text-sm outline-none transition-colors placeholder:text-muted-foreground focus:border-primary focus:ring-1 focus:ring-primary\"\n          defaultValue={userData.email}\n          id=\"email\"\n          name=\"email\"\n          placeholder=\"Enter your email\"\n          type=\"email\"\n        />\n      </div>\n\n      <button\n        className=\"mt-2 cursor-pointer rounded-md bg-brand px-4 py-2.5 font-semibold text-sm text-white shadow-sm transition-all hover:bg-brand/90 hover:shadow-md active:scale-[0.98] active:bg-brand\"\n        type=\"submit\"\n      >\n        Save Changes\n      </button>\n    </form>\n  );\n\n  const getStatusColor = (status: Order[\"status\"]) => {\n    if (status === \"processing\") {\n      return \"bg-blue-500\";\n    }\n    if (status === \"shipped\") {\n      return \"bg-amber-500\";\n    }\n    return \"bg-emerald-500\";\n  };\n\n  const renderLastOrders = () => (\n    <div className=\"flex flex-col gap-3 p-4\">\n      {orders.map((order) => (\n        <div\n          className=\"flex flex-col gap-3 rounded-lg border border-border bg-muted/30 p-3 transition-colors hover:bg-muted/50\"\n          key={order.id}\n        >\n          <div className=\"flex items-center justify-between\">\n            <div className=\"font-semibold text-sm\">{order.id}</div>\n            <div className=\"text-muted-foreground text-xs\">{order.date}</div>\n          </div>\n          <div className=\"flex items-center gap-3\">\n            <div className=\"flex-1 space-y-2\">\n              <div className=\"flex items-center justify-between text-xs\">\n                <span className=\"font-medium text-foreground capitalize\">\n                  {order.status}\n                </span>\n                <span className=\"text-muted-foreground\">{order.progress}%</span>\n              </div>\n              <div className=\"h-1.5 w-full overflow-hidden rounded-full bg-muted\">\n                <motion.div\n                  animate={\n                    shouldReduceMotion ? {} : { width: `${order.progress}%` }\n                  }\n                  className={`h-full rounded-full ${getStatusColor(order.status)}`}\n                  initial={shouldReduceMotion ? {} : { width: 0 }}\n                  transition={\n                    shouldReduceMotion\n                      ? { duration: 0 }\n                      : {\n                          damping: 30,\n                          duration: 0.4,\n                          stiffness: 300,\n                          type: \"spring\" as const,\n                        }\n                  }\n                />\n              </div>\n            </div>\n            <button\n              aria-label=\"View Order\"\n              className=\"flex shrink-0 cursor-pointer items-center justify-center rounded-md border border-border bg-background p-2 transition-colors hover:border-primary hover:bg-muted\"\n              onClick={() => {\n                onOrderView?.(order.id);\n              }}\n              type=\"button\"\n            >\n              <Eye className=\"text-muted-foreground\" size={16} />\n            </button>\n          </div>\n        </div>\n      ))}\n    </div>\n  );\n\n  return (\n    <PopoverRoot>\n      <PopoverTrigger asChild>\n        <button\n          className={`flex cursor-pointer items-center gap-2 rounded-full border bg-background ${className}`}\n          type=\"button\"\n        >\n          <img\n            alt=\"User Avatar\"\n            className=\"rounded-full\"\n            draggable={false}\n            height={48}\n            src={userData.avatar}\n            width={48}\n          />\n        </button>\n      </PopoverTrigger>\n      <PopoverPortal>\n        <PopoverContent\n          className=\"z-50 w-64 overflow-hidden rounded-xl border bg-background shadow-xl\"\n          onOpenAutoFocus={(e) => e.preventDefault()}\n          sideOffset={8}\n          style={{ pointerEvents: \"auto\" }}\n        >\n          <motion.div\n            animate={shouldReduceMotion ? {} : { height: \"auto\" }}\n            initial={shouldReduceMotion ? {} : { height: \"auto\" }}\n            style={{ pointerEvents: \"auto\" }}\n            transition={\n              shouldReduceMotion\n                ? { duration: 0 }\n                : { bounce: 0, duration: 0.25, type: \"spring\" as const }\n            }\n          >\n            <div\n              className=\"flex flex-col divide-y divide-border\"\n              style={{ pointerEvents: \"auto\" }}\n            >\n              <button\n                className={`flex w-full cursor-pointer items-center gap-2 rounded-lg px-3 py-2.5 font-medium text-sm transition-colors ${\n                  activeSection === \"profile\"\n                    ? \"bg-primary text-primary-foreground\"\n                    : \"text-foreground hover:bg-muted\"\n                }`}\n                onClick={() => {\n                  handleSectionClick(\"profile\");\n                }}\n                onKeyDown={(e) => {\n                  if (e.key === \"Enter\" || e.key === \" \") {\n                    e.preventDefault();\n                    handleSectionClick(\"profile\");\n                  }\n                }}\n                type=\"button\"\n              >\n                <User className=\"shrink-0\" size={16} />\n                Edit Profile\n              </button>\n              <AnimatePresence initial={false}>\n                {activeSection === \"profile\" && (\n                  <motion.div\n                    animate={\n                      shouldReduceMotion\n                        ? { height: \"auto\", opacity: 1 }\n                        : {\n                            filter: \"blur(0px)\",\n                            height: \"auto\",\n                            opacity: 1,\n                          }\n                    }\n                    exit={\n                      shouldReduceMotion\n                        ? { height: 0, opacity: 0, transition: { duration: 0 } }\n                        : { filter: \"blur(10px)\", height: 0, opacity: 0 }\n                    }\n                    initial={\n                      shouldReduceMotion\n                        ? { height: 0, opacity: 0 }\n                        : { filter: \"blur(10px)\", height: 0, opacity: 0 }\n                    }\n                    transition={\n                      shouldReduceMotion\n                        ? { duration: 0 }\n                        : { bounce: 0, duration: 0.25, type: \"spring\" as const }\n                    }\n                  >\n                    {renderEditProfile()}\n                  </motion.div>\n                )}\n              </AnimatePresence>\n              <button\n                className={`flex w-full cursor-pointer items-center gap-2 rounded-lg px-3 py-2.5 font-medium text-sm transition-colors ${\n                  activeSection === \"orders\"\n                    ? \"bg-primary text-primary-foreground\"\n                    : \"text-foreground hover:bg-muted\"\n                }`}\n                onClick={() => {\n                  handleSectionClick(\"orders\");\n                }}\n                onKeyDown={(e) => {\n                  if (e.key === \"Enter\" || e.key === \" \") {\n                    e.preventDefault();\n                    handleSectionClick(\"orders\");\n                  }\n                }}\n                type=\"button\"\n              >\n                <Package className=\"shrink-0\" size={16} />\n                Last Orders\n              </button>\n              <AnimatePresence initial={false}>\n                {activeSection === \"orders\" && (\n                  <motion.div\n                    animate={\n                      shouldReduceMotion\n                        ? { height: \"auto\", opacity: 1 }\n                        : {\n                            filter: \"blur(0px)\",\n                            height: \"auto\",\n                            opacity: 1,\n                          }\n                    }\n                    exit={\n                      shouldReduceMotion\n                        ? { height: 0, opacity: 0, transition: { duration: 0 } }\n                        : { filter: \"blur(10px)\", height: 0, opacity: 0 }\n                    }\n                    initial={\n                      shouldReduceMotion\n                        ? { height: 0, opacity: 0 }\n                        : { filter: \"blur(10px)\", height: 0, opacity: 0 }\n                    }\n                    transition={\n                      shouldReduceMotion\n                        ? { duration: 0 }\n                        : { bounce: 0, duration: 0.25, type: \"spring\" as const }\n                    }\n                  >\n                    {renderLastOrders()}\n                  </motion.div>\n                )}\n              </AnimatePresence>\n            </div>\n          </motion.div>\n        </PopoverContent>\n      </PopoverPortal>\n    </PopoverRoot>\n  );\n}\n","path":"index.tsx","target":"components/smoothui/user-account-avatar/index.tsx","type":"registry:ui"}],"name":"user-account-avatar","registryDependencies":["https://smoothui.dev/r/tokens.json"],"title":"User Account Avatar","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":[],"description":"A wavy perimeter circle reveal inspired by the Codrops circle warping step.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport SdfBlobTransition from \"../sdf-blob-transition\";\n\nexport interface WarpedCircleTransitionProps {\n  children: ReactNode;\n  className?: string;\n  duration?: number;\n  onRest?: () => void;\n  transitionKey: string | number;\n}\n\nconst WarpedCircleTransition = ({\n  children,\n  className,\n  duration = 1120,\n  onRest,\n  transitionKey,\n}: WarpedCircleTransitionProps) => (\n  <SdfBlobTransition\n    className={className}\n    duration={duration}\n    onRest={onRest}\n    transitionKey={transitionKey}\n    variant=\"warped\"\n  >\n    {children}\n  </SdfBlobTransition>\n);\n\nexport default WarpedCircleTransition;\n","path":"index.tsx","target":"components/smoothui/warped-circle-transition/index.tsx","type":"registry:ui"}],"name":"warped-circle-transition","registryDependencies":["https://smoothui.dev/r/sdf-blob-transition.json"],"title":"Warped Circle Transition","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"A WaveText component for SmoothUI.","devDependencies":[],"files":[{"content":"import { motion, useReducedMotion } from \"motion/react\";\nimport type React from \"react\";\n\nexport interface WaveTextProps {\n  amplitude?: number;\n  children: string;\n  className?: string;\n  duration?: number;\n  staggerDelay?: number;\n}\n\nconst WaveText: React.FC<WaveTextProps> = ({\n  children,\n  amplitude = 8,\n  duration = 1.2,\n  staggerDelay = 0.05,\n  className = \"\",\n}) => {\n  const shouldReduceMotion = useReducedMotion();\n\n  return (\n    <span className={className} style={{ display: \"inline-block\" }}>\n      {children.split(\"\").map((char, i) => (\n        <motion.span\n          animate={\n            shouldReduceMotion\n              ? { y: 0 }\n              : { y: [0, -amplitude, 0, amplitude * 0.5, 0] }\n          }\n          key={`${i}-${char}`}\n          style={{\n            display: \"inline-block\",\n            willChange: shouldReduceMotion ? undefined : \"transform\",\n          }}\n          transition={\n            shouldReduceMotion\n              ? { duration: 0 }\n              : {\n                  delay: i * staggerDelay,\n                  duration,\n                  ease: [0.37, 0, 0.63, 1],\n                  repeat: Number.POSITIVE_INFINITY,\n                  times: [0, 0.25, 0.5, 0.75, 1],\n                }\n          }\n        >\n          {char === \" \" ? \"\\u00A0\" : String(char)}\n        </motion.span>\n      ))}\n    </span>\n  );\n};\n\nexport default WaveText;\n","path":"index.tsx","target":"components/smoothui/wave-text/index.tsx","type":"registry:ui"}],"name":"wave-text","registryDependencies":[],"title":"Wave Text","type":"registry:ui"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":[],"description":"Shared animation constants (springs, easing curves, durations) for SmoothUI components","devDependencies":[],"files":[{"content":"/**\n * Shared animation constants for SmoothUI components.\n *\n * Spring configs follow project guidelines:\n * - Duration 0.2–0.25s for UI elements\n * - Bounce ≤ 0.1 for standard UI (higher only for playful/drag interactions)\n *\n * Easing curves use cubic-bezier arrays compatible with Motion's `ease` property.\n */\n\n/** Default spring for most UI animations */\nexport const SPRING_DEFAULT = {\n  bounce: 0.1,\n  duration: 0.25,\n  type: \"spring\" as const,\n};\n\n/** Snappy spring for quick, no-overshoot transitions */\nexport const SPRING_SNAPPY = {\n  bounce: 0,\n  duration: 0.2,\n  type: \"spring\" as const,\n};\n\n/** Ease-out curve for entering elements — cubic-bezier(.23, 1, .32, 1) */\nexport const EASE_OUT = [0.23, 1, 0.32, 1] as const;\n\n/** Ease-in-out curve for moving elements — cubic-bezier(0.645, 0.045, 0.355, 1) */\nexport const EASE_IN_OUT = [0.645, 0.045, 0.355, 1] as const;\n\n/** Instant transition for reduced-motion contexts */\nexport const DURATION_INSTANT = { duration: 0 };\n\n/** Standard animation durations (seconds) */\nexport const DURATION = {\n  complex: 0.4,\n  default: 0.25,\n  fast: 0.15,\n  slow: 0.3,\n} as const;\n\nexport type SpringConfig = typeof SPRING_DEFAULT;\nexport type EasingCurve = readonly [number, number, number, number];\n","path":"animation.ts","target":"components/smoothui/lib/animation.ts","type":"registry:lib"}],"name":"lib","registryDependencies":[],"title":"Lib","type":"registry:lib"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["lucide-react","motion"],"description":"Full ChatGPT-style chat surface — sidebar, transcript and composer — wired to a simulated agent so every AI component is on screen.","devDependencies":[],"files":[{"content":"import type { AIApprovalOption } from \"@/components/smoothui/ai-approval\";\nimport type { AIDiffLine } from \"@/components/smoothui/ai-diff\";\nimport type { AIPromptAttachment } from \"@/components/smoothui/ai-prompt-input\";\nimport type { AIResponseCitation } from \"@/components/smoothui/ai-response\";\nimport type { AISource } from \"@/components/smoothui/ai-sources\";\nimport type { AISuggestion } from \"@/components/smoothui/ai-suggestions\";\nimport type { AITask } from \"@/components/smoothui/ai-task-list\";\n\n/**\n * A transcript turn.\n *\n * Assistant turns are a bag of optional parts rather than a block of markdown,\n * because that is what an agent turn actually is: some thinking, some tool\n * calls, some produced artifact, then prose. Adding a new part means adding one\n * field here and one branch in `chat-thread.tsx` — that is the extension point\n * as more of the AI set lands.\n */\nexport type ChatTurn =\n  | {\n      /** Files that rode along with the message. */\n      attachments?: AIPromptAttachment[];\n      from: \"user\";\n      id: string;\n      text: string;\n      timestamp: string;\n    }\n  | {\n      approval?: { options: AIApprovalOption[]; question: string };\n      artifact?: { code: string; language: string; title: string };\n      citations?: AIResponseCitation[];\n      diff?: { lines: AIDiffLine[]; title: string };\n      from: \"assistant\";\n      id: string;\n      reasoning?: string;\n      sources?: AISource[];\n      suggestions?: AISuggestion[];\n      tasks?: { label: string; tasks: AITask[] };\n      text?: string;\n      timestamp: string;\n      tool?: { args: string; name: string; result: string; summary: string };\n    };\n\nexport type ChatConversation = {\n  /** Sidebar grouping label — real products group by recency, so this does. */\n  group: string;\n  id: string;\n  title: string;\n  turns: ChatTurn[];\n};\n\nconst RETIRE_FLAVOUR: ChatTurn[] = [\n  {\n    attachments: [\n      { id: \"a1\", name: \"sales-velocity-2026-q2.csv\", size: 24_600 },\n      { id: \"a2\", name: \"flavour-review.pdf\", size: 1_248_000 },\n    ],\n    from: \"user\",\n    id: \"r1\",\n    text: \"Compare mint chip to last summer and tell me which flavour to retire before Q4.\",\n    timestamp: \"14:31\",\n  },\n  {\n    citations: [\n      { id: \"1\", index: 1, title: \"Sales velocity export, 2026 Q2\" },\n      { id: \"2\", index: 2, title: \"Flavour performance review\" },\n    ],\n    from: \"assistant\",\n    id: \"r2\",\n    reasoning:\n      \"Pulled three summers of sales, normalised for the two stores that opened last year, then compared weekend and weekday velocity before ranking the classics.\",\n    sources: [\n      {\n        id: \"s1\",\n        snippet: \"Weekly units by flavour and store, 2024–2026.\",\n        title: \"Sales velocity export, 2026 Q2\",\n        url: \"internal://warehouse/sales-velocity-2026-q2.csv\",\n      },\n      {\n        id: \"s2\",\n        snippet: \"Margin per scoop after the dairy contract renewal.\",\n        title: \"Flavour performance review\",\n        url: \"internal://docs/flavour-performance-review.md\",\n      },\n    ],\n    suggestions: [\n      { id: \"g1\", label: \"Draft the retirement announcement\" },\n      { id: \"g2\", label: \"What replaces rocky road?\" },\n      { id: \"g3\", label: \"Show the margin per scoop\" },\n    ],\n    text: \"Mint chip is up 12% on last summer, and the whole gain sits on weekends [1]. Rocky road is the one to retire — down 6% year on year, lowest margin per scoop of the classics, and it does not recover in any store [2].\",\n    timestamp: \"14:32\",\n    tool: {\n      args: '{ \"flavours\": [\"mint-chip\", \"rocky-road\"], \"years\": 3 }',\n      name: \"query_sales\",\n      result: \"412 rows\",\n      summary: \"3 summers\",\n    },\n  },\n];\n\nconst SHIP_PRICE_CHANGE: ChatTurn[] = [\n  {\n    from: \"user\",\n    id: \"p1\",\n    text: \"Raise the single scoop to 4.20 everywhere and stage the change.\",\n    timestamp: \"11:04\",\n  },\n  {\n    approval: {\n      options: [\n        { id: \"ship\", label: \"Apply to all 14 stores\" },\n        {\n          detail: \"Rolls out to the two pilot stores only\",\n          id: \"pilot\",\n          label: \"Pilot first\",\n        },\n        { destructive: true, id: \"discard\", label: \"Discard the change\" },\n      ],\n      question: \"This updates live prices in 14 stores. Apply now?\",\n    },\n    diff: {\n      lines: [\n        { content: \"  scoops:\", kind: \"context\", number: 12 },\n        { content: \"-   single: 3.90\", kind: \"removed\", number: 13 },\n        { content: \"+   single: 4.20\", kind: \"added\", number: 13 },\n        { content: \"    double: 6.40\", kind: \"context\", number: 14 },\n        { content: \"-   kids: 2.60\", kind: \"removed\", number: 15 },\n        { content: \"+   kids: 2.80\", kind: \"added\", number: 15 },\n      ],\n      title: \"config/pricing.yaml\",\n    },\n    from: \"assistant\",\n    id: \"p2\",\n    tasks: {\n      label: \"Plan\",\n      tasks: [\n        { id: \"t1\", label: \"Read current price table\", status: \"done\" },\n        {\n          children: [\n            { id: \"t2a\", label: \"Update scoop tiers\", status: \"done\" },\n            { id: \"t2b\", label: \"Update kids portion\", status: \"done\" },\n          ],\n          id: \"t2\",\n          label: \"Stage the new prices\",\n          note: \"2 files\",\n          status: \"done\",\n        },\n        { id: \"t3\", label: \"Wait for approval\", status: \"running\" },\n        {\n          id: \"t4\",\n          label: \"Publish to stores\",\n          note: \"14 stores\",\n          status: \"pending\",\n        },\n      ],\n    },\n    text: \"Staged. Kids portion moves with it to keep the ratio you set last spring — say the word and I will publish.\",\n    timestamp: \"11:05\",\n  },\n];\n\nconst SUMMER_CAMPAIGN: ChatTurn[] = [\n  {\n    from: \"user\",\n    id: \"c1\",\n    text: \"Give me a summer campaign banner I can drop into the site.\",\n    timestamp: \"09:12\",\n  },\n  {\n    artifact: {\n      code: `export const SummerBanner = () => (\n  <section className=\"rounded-3xl bg-gradient-to-br from-pink-200 to-sky-200 p-10\">\n    <p className=\"text-sm uppercase tracking-widest\">Summer 2026</p>\n    <h2 className=\"mt-2 text-4xl font-semibold\">Two scoops, one price</h2>\n    <p className=\"mt-3 max-w-sm text-sm\">\n      Every weekday before 5pm, all summer.\n    </p>\n  </section>\n);`,\n      language: \"tsx\",\n      title: \"SummerBanner.tsx\",\n    },\n    from: \"assistant\",\n    id: \"c2\",\n    suggestions: [\n      { id: \"c-s1\", label: \"Make it dark mode aware\" },\n      { id: \"c-s2\", label: \"Add a countdown\" },\n    ],\n    text: \"Here it is, using the pink and sky pair from your brand tokens rather than new colours.\",\n    timestamp: \"09:13\",\n  },\n];\n\nexport const CONVERSATIONS: ChatConversation[] = [\n  {\n    group: \"Today\",\n    id: \"retire-flavour\",\n    title: \"Which flavour to retire\",\n    turns: RETIRE_FLAVOUR,\n  },\n  {\n    group: \"Today\",\n    id: \"price-change\",\n    title: \"Stage the price change\",\n    turns: SHIP_PRICE_CHANGE,\n  },\n  {\n    group: \"Previous 7 days\",\n    id: \"summer-campaign\",\n    title: \"Summer campaign banner\",\n    turns: SUMMER_CAMPAIGN,\n  },\n];\n\n/** Follow-ups offered on an empty thread, so the composer is never a blank stare. */\nexport const STARTER_SUGGESTIONS: AISuggestion[] = [\n  { id: \"st1\", label: \"Which store is growing fastest?\" },\n  { id: \"st2\", label: \"Draft next week's staff rota\" },\n  { id: \"st3\", label: \"Summarise last month's reviews\" },\n];\n\n/**\n * The reply the template plays back for anything you type.\n *\n * There is no model behind this. Everything below is a fixed script, which is\n * the honest way to demo a chat surface: the components are the product, the\n * answer is set dressing.\n */\nexport const SIMULATED_REPLY = {\n  citations: [\n    { id: \"1\", index: 1, title: \"Store operations log, week 30\" },\n  ] satisfies AIResponseCitation[],\n  reasoning:\n    \"Checked the four stores that reported this week, then compared footfall against the same week last year before answering.\",\n  sources: [\n    {\n      id: \"sim-s1\",\n      snippet: \"Hourly footfall and till receipts, week 30.\",\n      title: \"Store operations log, week 30\",\n      url: \"internal://warehouse/ops-log-w30.csv\",\n    },\n  ] satisfies AISource[],\n  suggestions: [\n    { id: \"sim-g1\", label: \"Break it down by store\" },\n    { id: \"sim-g2\", label: \"Compare with last summer\" },\n  ] satisfies AISuggestion[],\n  text: \"Weekday afternoons are the soft spot: footfall holds but the average ticket drops about 18% after 3pm [1]. A two-scoop weekday offer is the cheapest lever you have before Q4.\",\n  tool: {\n    args: '{ \"week\": 30, \"stores\": \"all\" }',\n    name: \"query_operations\",\n    result: \"96 rows\",\n    summary: \"week 30\",\n  },\n} as const;\n\n/** Offered by the composer's attach control — a picker stands in for a file dialog. */\nexport const ATTACHABLE_FILES: AIPromptAttachment[] = [\n  { id: \"f1\", name: \"ops-log-w30.csv\", size: 18_400 },\n  { id: \"f2\", name: \"staff-rota-august.xlsx\", size: 42_900 },\n  { id: \"f3\", name: \"supplier-invoice-3312.pdf\", size: 268_000 },\n];\n\nexport const MODELS = [\n  { id: \"opus-5\", label: \"Opus 5\", note: \"Best for planning\" },\n  { id: \"sonnet-5\", label: \"Sonnet 5\", note: \"Fast, everyday work\" },\n  { id: \"haiku-4-5\", label: \"Haiku 4.5\", note: \"Cheapest\" },\n] as const;\n\nexport const CONTEXT_LIMIT = 200_000;\n","path":"chat-data.ts","target":"components/smoothui/chat-template/chat-data.ts","type":"registry:block"},{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport SiriOrb from \"@/components/smoothui/siri-orb\";\nimport {\n  LogOut,\n  Moon,\n  PanelLeftClose,\n  PanelLeftOpen,\n  Plus,\n  Search,\n  Settings,\n  Sun,\n} from \"lucide-react\";\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport type { ChatConversation } from \"./chat-data\";\n\n/** A real photograph, so the footer is a person and not a lettered circle. */\nconst USER_AVATAR =\n  \"https://ik.imagekit.io/16u211libb/avatar-educalvolpz.jpeg?tr=w-64,h-64\";\n\nexport type ChatSidebarProps = {\n  activeId: string;\n  className?: string;\n  /** Icon rail instead of the full pane. Keeps navigation reachable. */\n  collapsed?: boolean;\n  conversations: ChatConversation[];\n  onNewChat: () => void;\n  onSelect: (id: string) => void;\n  onToggleCollapsed?: () => void;\n};\n\nexport const ChatSidebar = ({\n  activeId,\n  className,\n  collapsed = false,\n  conversations,\n  onNewChat,\n  onSelect,\n  onToggleCollapsed,\n}: ChatSidebarProps) => {\n  const [query, setQuery] = useState(\"\");\n  const searchRef = useRef<HTMLInputElement>(null);\n\n  // The search filters for real. A search box that does nothing is the kind of\n  // fake content this template exists to avoid.\n  const groups = useMemo(() => {\n    const needle = query.trim().toLowerCase();\n    const matching = needle\n      ? conversations.filter((conversation) =>\n          conversation.title.toLowerCase().includes(needle)\n        )\n      : conversations;\n\n    const byGroup = new Map<string, ChatConversation[]>();\n    for (const conversation of matching) {\n      const bucket = byGroup.get(conversation.group) ?? [];\n      bucket.push(conversation);\n      byGroup.set(conversation.group, bucket);\n    }\n    return [...byGroup.entries()];\n  }, [conversations, query]);\n\n  if (collapsed) {\n    return (\n      <aside\n        className={cn(\n          \"flex h-full w-[4.5rem] shrink-0 flex-col items-center gap-2 border-border/60 border-r bg-muted/60 py-3\",\n          className\n        )}\n      >\n        <SiriOrb size=\"22px\" state=\"idle\" />\n        <RailButton\n          icon={<PanelLeftOpen aria-hidden=\"true\" size={16} />}\n          label=\"Expand sidebar\"\n          onClick={onToggleCollapsed}\n        />\n        <RailButton\n          icon={<Plus aria-hidden=\"true\" size={16} />}\n          label=\"New chat\"\n          onClick={onNewChat}\n        />\n        <RailButton\n          icon={<Search aria-hidden=\"true\" size={16} />}\n          label=\"Search chats\"\n          onClick={() => {\n            onToggleCollapsed?.();\n            // Expanding and focusing in one action, so the icon is a shortcut\n            // rather than a two-step detour.\n            requestAnimationFrame(() => searchRef.current?.focus());\n          }}\n        />\n        <div className=\"mt-auto\">\n          <AccountMenu align=\"rail\" />\n        </div>\n      </aside>\n    );\n  }\n\n  return (\n    <aside\n      className={cn(\n        \"flex h-full w-[17rem] shrink-0 flex-col gap-3 border-border/60 border-r bg-muted/60 p-3\",\n        className\n      )}\n    >\n      <div className=\"flex items-center justify-between gap-2 px-1\">\n        <span className=\"flex items-center gap-2 font-medium text-sm\">\n          <SiriOrb size=\"22px\" state=\"idle\" />\n          Scoop Assistant\n        </span>\n        <button\n          aria-label=\"Collapse sidebar\"\n          className=\"rounded-lg p-1.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground\"\n          onClick={onToggleCollapsed}\n          type=\"button\"\n        >\n          <PanelLeftClose aria-hidden=\"true\" size={16} />\n        </button>\n      </div>\n\n      <button\n        className=\"flex items-center gap-2 rounded-xl border border-border/60 bg-background px-3 py-2 text-left text-sm transition-colors hover:bg-muted\"\n        onClick={onNewChat}\n        type=\"button\"\n      >\n        <Plus aria-hidden=\"true\" size={15} />\n        New chat\n      </button>\n\n      {/* Icon and field share one flex row rather than absolute-positioning the\n          icon over padding: the gap is then a single value instead of two that\n          have to be kept in sync, and `type=\"search\"` cannot push the text away\n          with its own intrinsic padding. */}\n      <label className=\"flex items-center gap-2 rounded-xl border border-transparent bg-muted px-2.5 py-2 transition-colors focus-within:border-border focus-within:bg-background\">\n        <span className=\"sr-only\">Search chats</span>\n        <Search\n          aria-hidden=\"true\"\n          className=\"shrink-0 text-muted-foreground\"\n          size={14}\n        />\n        <input\n          className=\"w-full appearance-none bg-transparent text-sm outline-none placeholder:text-muted-foreground\"\n          onChange={(event) => setQuery(event.target.value)}\n          placeholder=\"Search chats\"\n          ref={searchRef}\n          type=\"search\"\n          value={query}\n        />\n      </label>\n\n      <nav className=\"-mx-1 flex-1 overflow-y-auto px-1\">\n        {groups.length === 0 && (\n          <p className=\"px-2 py-6 text-center text-muted-foreground text-xs\">\n            No chats match “{query}”.\n          </p>\n        )}\n\n        {groups.map(([group, items]) => (\n          <div className=\"mb-3\" key={group}>\n            <p className=\"px-2 pt-1 pb-1.5 font-medium text-[0.7rem] text-muted-foreground/80 uppercase tracking-wide\">\n              {group}\n            </p>\n            <ul className=\"flex flex-col gap-0.5\">\n              {items.map((conversation) => (\n                <li key={conversation.id}>\n                  <button\n                    aria-current={\n                      conversation.id === activeId ? \"page\" : undefined\n                    }\n                    className={cn(\n                      \"w-full truncate rounded-lg px-2 py-2 text-left text-sm transition-colors\",\n                      conversation.id === activeId\n                        ? \"bg-background text-foreground shadow-black/5 shadow-xs\"\n                        : \"text-muted-foreground hover:bg-muted hover:text-foreground\"\n                    )}\n                    onClick={() => onSelect(conversation.id)}\n                    type=\"button\"\n                  >\n                    {conversation.title}\n                  </button>\n                </li>\n              ))}\n            </ul>\n          </div>\n        ))}\n      </nav>\n\n      <div className=\"border-border/60 border-t pt-3\">\n        <AccountMenu align=\"pane\" />\n      </div>\n    </aside>\n  );\n};\n\nconst RailButton = ({\n  icon,\n  label,\n  onClick,\n}: {\n  icon: React.ReactNode;\n  label: string;\n  onClick?: () => void;\n}) => (\n  <button\n    aria-label={label}\n    className=\"rounded-lg p-2 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground\"\n    onClick={onClick}\n    title={label}\n    type=\"button\"\n  >\n    {icon}\n  </button>\n);\n\n/**\n * The account menu, with the theme switch inside it.\n *\n * It flips the `dark` class on the document root rather than shipping a theme\n * provider: that is the one contract every Tailwind setup already has, so the\n * control works the moment the template is installed instead of after wiring.\n */\nconst AccountMenu = ({ align }: { align: \"pane\" | \"rail\" }) => {\n  const [isOpen, setIsOpen] = useState(false);\n  const [isDark, setIsDark] = useState(false);\n  const containerRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    setIsDark(document.documentElement.classList.contains(\"dark\"));\n  }, []);\n\n  useEffect(() => {\n    if (!isOpen) {\n      return;\n    }\n\n    const onPointerDown = (event: PointerEvent) => {\n      if (!containerRef.current?.contains(event.target as Node)) {\n        setIsOpen(false);\n      }\n    };\n    const onKeyDown = (event: KeyboardEvent) => {\n      if (event.key === \"Escape\") {\n        setIsOpen(false);\n      }\n    };\n\n    document.addEventListener(\"pointerdown\", onPointerDown);\n    document.addEventListener(\"keydown\", onKeyDown);\n    return () => {\n      document.removeEventListener(\"pointerdown\", onPointerDown);\n      document.removeEventListener(\"keydown\", onKeyDown);\n    };\n  }, [isOpen]);\n\n  const toggleTheme = () => {\n    const next = !document.documentElement.classList.contains(\"dark\");\n    document.documentElement.classList.toggle(\"dark\", next);\n    setIsDark(next);\n  };\n\n  return (\n    <div className=\"relative\" ref={containerRef}>\n      <button\n        aria-expanded={isOpen}\n        aria-haspopup=\"menu\"\n        className={cn(\n          \"flex w-full items-center gap-2 rounded-xl text-left transition-colors hover:bg-muted\",\n          align === \"pane\" ? \"p-1.5\" : \"justify-center p-1\"\n        )}\n        onClick={() => setIsOpen((open) => !open)}\n        type=\"button\"\n      >\n        <img\n          alt=\"Edu Calvo\"\n          className=\"size-7 shrink-0 rounded-full object-cover\"\n          height={28}\n          src={USER_AVATAR}\n          width={28}\n        />\n        {align === \"pane\" && (\n          <span className=\"min-w-0 flex-1\">\n            <span className=\"block truncate text-sm\">Edu Calvo</span>\n            <span className=\"block text-muted-foreground text-xs\">\n              Pro plan\n            </span>\n          </span>\n        )}\n      </button>\n\n      {isOpen ? (\n        <div\n          className=\"absolute bottom-full left-0 z-10 mb-2 w-52 origin-bottom-left overflow-hidden rounded-xl border border-border/60 bg-background p-1 shadow-black/10 shadow-lg\"\n          role=\"menu\"\n        >\n          <MenuItem\n            icon={\n              isDark ? (\n                <Sun aria-hidden=\"true\" size={14} />\n              ) : (\n                <Moon aria-hidden=\"true\" size={14} />\n              )\n            }\n            label={isDark ? \"Light mode\" : \"Dark mode\"}\n            onClick={toggleTheme}\n          />\n          <MenuItem\n            icon={<Settings aria-hidden=\"true\" size={14} />}\n            label=\"Settings\"\n          />\n          <MenuItem\n            icon={<LogOut aria-hidden=\"true\" size={14} />}\n            label=\"Sign out\"\n          />\n        </div>\n      ) : null}\n    </div>\n  );\n};\n\nconst MenuItem = ({\n  icon,\n  label,\n  onClick,\n}: {\n  icon: React.ReactNode;\n  label: string;\n  onClick?: () => void;\n}) => (\n  <button\n    className=\"flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left text-muted-foreground text-sm transition-colors hover:bg-muted hover:text-foreground\"\n    onClick={onClick}\n    role=\"menuitem\"\n    type=\"button\"\n  >\n    {icon}\n    {label}\n  </button>\n);\n\nexport default ChatSidebar;\n","path":"chat-sidebar.tsx","target":"components/smoothui/chat-template/chat-sidebar.tsx","type":"registry:block"},{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport AIApproval from \"@/components/smoothui/ai-approval\";\nimport AIArtifact from \"@/components/smoothui/ai-artifact\";\nimport AIContextMeter from \"@/components/smoothui/ai-context-meter\";\nimport AIConversation from \"@/components/smoothui/ai-conversation\";\nimport type { AIState } from \"@/components/smoothui/ai-core\";\nimport AIDiff from \"@/components/smoothui/ai-diff\";\nimport AILoader from \"@/components/smoothui/ai-loader\";\nimport AIMessage from \"@/components/smoothui/ai-message\";\nimport AIPromptInput, {\n  type AIPromptAttachment,\n} from \"@/components/smoothui/ai-prompt-input\";\nimport AIReasoning from \"@/components/smoothui/ai-reasoning\";\nimport AIResponse from \"@/components/smoothui/ai-response\";\nimport AISources from \"@/components/smoothui/ai-sources\";\nimport AISuggestions from \"@/components/smoothui/ai-suggestions\";\nimport AITaskList from \"@/components/smoothui/ai-task-list\";\nimport AIToolCall from \"@/components/smoothui/ai-tool-call\";\nimport SiriOrb from \"@/components/smoothui/siri-orb\";\nimport { ChevronDown, PanelLeftOpen, Paperclip } from \"lucide-react\";\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport {\n  ATTACHABLE_FILES,\n  type ChatTurn,\n  CONTEXT_LIMIT,\n  MODELS,\n  SIMULATED_REPLY,\n  STARTER_SUGGESTIONS,\n} from \"./chat-data\";\n\nconst THINK_MS = 900;\nconst TOOL_MS = 1900;\nconst STREAM_INTERVAL_MS = 45;\n/** Rough English ratio, good enough for a meter that is showing pressure. */\nconst CHARS_PER_TOKEN = 4;\nconst SYSTEM_TOKENS = 1800;\nconst TOKENS_PER_SOURCE = 9200;\nconst TOKENS_PER_ATTACHMENT = 6400;\n\ntype LivePhase = \"idle\" | \"thinking\" | \"tool\" | \"streaming\";\n\nconst formatClock = (date: Date) =>\n  `${String(date.getHours()).padStart(2, \"0\")}:${String(date.getMinutes()).padStart(2, \"0\")}`;\n\nexport type ChatThreadProps = {\n  className?: string;\n  /** Opens the conversation list on narrow screens, where it has no column. */\n  onOpenSidebar?: () => void;\n  title: string;\n  turns: ChatTurn[];\n};\n\nexport const ChatThread = ({\n  className,\n  onOpenSidebar,\n  title,\n  turns,\n}: ChatThreadProps) => {\n  const [localTurns, setLocalTurns] = useState<ChatTurn[]>([]);\n  const [phase, setPhase] = useState<LivePhase>(\"idle\");\n  const [streamed, setStreamed] = useState(\"\");\n  const [draftAttachments, setDraftAttachments] = useState<\n    AIPromptAttachment[]\n  >([]);\n  const [model, setModel] = useState<string>(MODELS[0].label);\n  const timers = useRef<ReturnType<typeof setTimeout>[]>([]);\n\n  const clearTimers = useCallback(() => {\n    for (const timer of timers.current) {\n      clearTimeout(timer);\n    }\n    timers.current = [];\n  }, []);\n\n  useEffect(() => clearTimers, [clearTimers]);\n\n  useEffect(() => {\n    if (phase !== \"streaming\") {\n      return;\n    }\n    const words = SIMULATED_REPLY.text.split(\" \");\n    let index = 0;\n    const interval = setInterval(() => {\n      index += 1;\n      setStreamed(words.slice(0, index).join(\" \"));\n      if (index >= words.length) {\n        clearInterval(interval);\n        setPhase(\"idle\");\n        setLocalTurns((current) => [\n          ...current,\n          {\n            citations: [...SIMULATED_REPLY.citations],\n            from: \"assistant\",\n            id: `live-a-${current.length}`,\n            reasoning: SIMULATED_REPLY.reasoning,\n            sources: [...SIMULATED_REPLY.sources],\n            suggestions: [...SIMULATED_REPLY.suggestions],\n            text: SIMULATED_REPLY.text,\n            timestamp: formatClock(new Date()),\n            tool: { ...SIMULATED_REPLY.tool },\n          },\n        ]);\n        setStreamed(\"\");\n      }\n    }, STREAM_INTERVAL_MS);\n    return () => clearInterval(interval);\n  }, [phase]);\n\n  const send = (value: string) => {\n    const draft = value.trim();\n    if (!draft || phase !== \"idle\") {\n      return;\n    }\n\n    clearTimers();\n    const attachments = draftAttachments;\n    setDraftAttachments([]);\n    setLocalTurns((current) => [\n      ...current,\n      {\n        attachments: attachments.length > 0 ? attachments : undefined,\n        from: \"user\",\n        id: `live-u-${current.length}`,\n        text: draft,\n        timestamp: formatClock(new Date()),\n      },\n    ]);\n    setPhase(\"thinking\");\n    // Stand-ins for a request's stages. No model runs here.\n    timers.current.push(setTimeout(() => setPhase(\"tool\"), THINK_MS));\n    timers.current.push(setTimeout(() => setPhase(\"streaming\"), TOOL_MS));\n  };\n\n  const stop = () => {\n    clearTimers();\n    setPhase(\"idle\");\n    setStreamed(\"\");\n  };\n\n  // Memoised because the token breakdown below depends on it; a fresh array each\n  // render would make that memo pointless.\n  const allTurns = useMemo(\n    () => [...turns, ...localTurns],\n    [turns, localTurns]\n  );\n  const isBusy = phase !== \"idle\";\n  const state: AIState = phase === \"streaming\" ? \"streaming\" : \"thinking\";\n  // Derived from what is actually on screen, so an empty chat reads as empty\n  // instead of inheriting a fixture's 67k. A fixed number here was a lie the\n  // moment you pressed \"New chat\".\n  const breakdown = useMemo(() => {\n    // Everything the model would actually have been sent, not just the prose:\n    // a reasoning trace and an artifact's source are the bulk of a real turn.\n    const transcript =\n      allTurns.reduce((total, turn) => {\n        if (turn.from === \"user\") {\n          return total + turn.text.length;\n        }\n        return (\n          total +\n          (turn.text?.length ?? 0) +\n          (turn.reasoning?.length ?? 0) +\n          (turn.artifact?.code.length ?? 0) +\n          (turn.tool ? turn.tool.args.length + turn.tool.result.length : 0) +\n          (turn.diff?.lines.reduce(\n            (chars, line) => chars + line.content.length,\n            0\n          ) ?? 0)\n        );\n      }, 0) / CHARS_PER_TOKEN;\n    const retrieved = allTurns.reduce(\n      (total, turn) =>\n        total +\n        (turn.from === \"assistant\" && turn.sources\n          ? turn.sources.length * TOKENS_PER_SOURCE\n          : 0) +\n        (turn.from === \"user\" && turn.attachments\n          ? turn.attachments.length * TOKENS_PER_ATTACHMENT\n          : 0),\n      0\n    );\n\n    return [\n      { label: \"System\", tokens: SYSTEM_TOKENS },\n      {\n        label: \"Transcript\",\n        tokens: Math.round(transcript + streamed.length / CHARS_PER_TOKEN),\n      },\n      ...(retrieved > 0\n        ? [{ label: \"Retrieved files\", tokens: retrieved }]\n        : []),\n    ];\n  }, [allTurns, streamed]);\n\n  const used = breakdown.reduce((total, item) => total + item.tokens, 0);\n\n  return (\n    <section className={cn(\"flex min-w-0 flex-1 flex-col\", className)}>\n      <header className=\"flex items-center gap-3 border-border/60 border-b px-4 py-2.5\">\n        {/* The only way into the conversation list on a phone, where the sidebar\n            has no column of its own. */}\n        {onOpenSidebar ? (\n          <button\n            aria-label=\"Open conversations\"\n            className=\"-ml-1 flex cursor-pointer items-center rounded-lg p-1.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground md:hidden\"\n            onClick={onOpenSidebar}\n            type=\"button\"\n          >\n            <PanelLeftOpen aria-hidden=\"true\" size={16} />\n          </button>\n        ) : null}\n        <h2 className=\"min-w-0 flex-1 truncate font-medium text-sm\">{title}</h2>\n        <AIContextMeter\n          breakdown={breakdown}\n          limit={CONTEXT_LIMIT}\n          used={used}\n        />\n      </header>\n\n      <AIConversation\n        className=\"flex-1 px-4\"\n        contentKey={`${allTurns.length}-${streamed.length}-${phase}`}\n      >\n        {/* `px-2`, not decoration: the scroller clips at its padding box, and the\n              orb avatar sat flush against that edge — its glow extends past its\n              own 26px box, so the left of it was being shaved off. */}\n        <div className=\"mx-auto flex min-h-full w-full max-w-2xl flex-col gap-5 px-2 py-5\">\n          {allTurns.length === 0 && (\n            <div className=\"flex flex-1 flex-col items-center justify-center gap-4 py-10 text-center\">\n              <SiriOrb size=\"64px\" state=\"idle\" />\n              <p className=\"text-muted-foreground text-sm\">\n                Ask about sales, stores or staffing.\n              </p>\n              <AISuggestions\n                onSelect={(suggestion) => send(suggestion.label)}\n                suggestions={STARTER_SUGGESTIONS}\n              />\n            </div>\n          )}\n\n          {allTurns.map((turn) => (\n            <ChatTurnView key={turn.id} onSuggestion={send} turn={turn} />\n          ))}\n\n          {isBusy && (\n            <AIMessage\n              avatar={<SiriOrb size=\"26px\" state={state} />}\n              bubble={false}\n              from=\"assistant\"\n            >\n              <div className=\"flex flex-col gap-3\">\n                <AIReasoning collapseWhenDone isStreaming>\n                  {SIMULATED_REPLY.reasoning}\n                </AIReasoning>\n\n                {phase !== \"thinking\" && (\n                  <AIToolCall\n                    args={<code>{SIMULATED_REPLY.tool.args}</code>}\n                    name={SIMULATED_REPLY.tool.name}\n                    result={<span>{SIMULATED_REPLY.tool.result}</span>}\n                    status={phase === \"tool\" ? \"running\" : \"success\"}\n                    summary={SIMULATED_REPLY.tool.summary}\n                  />\n                )}\n\n                {phase === \"streaming\" ? (\n                  <AIResponse\n                    citations={[...SIMULATED_REPLY.citations]}\n                    isStreaming\n                    text={streamed}\n                  />\n                ) : (\n                  <AILoader label=\"Thinking\" showElapsed variant=\"dots\" />\n                )}\n              </div>\n            </AIMessage>\n          )}\n        </div>\n      </AIConversation>\n\n      <div className=\"border-border/60 border-t px-4 py-3\">\n        <div className=\"mx-auto w-full max-w-2xl\">\n          <AIPromptInput\n            attachments={draftAttachments}\n            maxLength={2000}\n            onAttach={() => {\n              // Stands in for a file dialog: adds the next unattached file, so\n              // the control does something real instead of miming it.\n              setDraftAttachments((current) => {\n                const next = ATTACHABLE_FILES.find(\n                  (file) => !current.some((item) => item.id === file.id)\n                );\n                return next ? [...current, next] : current;\n              });\n            }}\n            onRemoveAttachment={(id) =>\n              setDraftAttachments((current) =>\n                current.filter((file) => file.id !== id)\n              )\n            }\n            onStop={stop}\n            onSubmit={send}\n            placeholder=\"Ask anything about the shop…\"\n            state={isBusy ? \"streaming\" : \"idle\"}\n          >\n            <ModelPicker onSelect={setModel} value={model} />\n          </AIPromptInput>\n          <p className=\"pt-2 text-center text-muted-foreground text-xs\">\n            Simulated responses. Nothing is sent anywhere.\n          </p>\n        </div>\n      </div>\n    </section>\n  );\n};\n\n/**\n * One transcript turn.\n *\n * Every assistant part is optional and rendered in the order an agent produces\n * it: thinking, tools, plan, changes, artifact, prose, then what it read. New AI\n * components slot in as one more branch here.\n */\nconst ChatTurnView = ({\n  onSuggestion,\n  turn,\n}: {\n  onSuggestion: (value: string) => void;\n  turn: ChatTurn;\n}) => {\n  if (turn.from === \"user\") {\n    return (\n      <AIMessage copyText={turn.text} from=\"user\" timestamp={turn.timestamp}>\n        <span className=\"flex flex-col gap-2\">\n          {turn.text}\n          {turn.attachments && turn.attachments.length > 0 && (\n            <span className=\"flex flex-wrap justify-end gap-1.5\">\n              {turn.attachments.map((file) => (\n                <span\n                  className=\"flex items-center gap-1.5 rounded-lg bg-background/15 px-2 py-1 text-xs\"\n                  key={file.id}\n                >\n                  <Paperclip aria-hidden=\"true\" size={11} />\n                  {file.name}\n                  <span className=\"opacity-70\">{formatBytes(file.size)}</span>\n                </span>\n              ))}\n            </span>\n          )}\n        </span>\n      </AIMessage>\n    );\n  }\n\n  return (\n    <div className=\"flex flex-col gap-3\">\n      <AIMessage\n        avatar={<SiriOrb size=\"26px\" state=\"done\" />}\n        bubble={false}\n        copyText={turn.text}\n        from=\"assistant\"\n        onRetry={() => {\n          // A replayed transcript has nothing to retry against.\n        }}\n        onVote={() => {\n          // Demo only — no telemetry leaves the page.\n        }}\n        timestamp={turn.timestamp}\n      >\n        <div className=\"flex flex-col gap-3\">\n          {turn.reasoning ? (\n            <AIReasoning collapseWhenDone duration={4}>\n              {turn.reasoning}\n            </AIReasoning>\n          ) : null}\n\n          {turn.tool ? (\n            <AIToolCall\n              args={<code>{turn.tool.args}</code>}\n              name={turn.tool.name}\n              result={<span>{turn.tool.result}</span>}\n              status=\"success\"\n              summary={turn.tool.summary}\n            />\n          ) : null}\n\n          {turn.tasks ? (\n            <AITaskList label={turn.tasks.label} tasks={turn.tasks.tasks} />\n          ) : null}\n\n          {turn.diff ? (\n            <AIDiff lines={turn.diff.lines} title={turn.diff.title} />\n          ) : null}\n\n          {turn.artifact ? (\n            <AIArtifact\n              code={\n                <pre className=\"whitespace-pre-wrap\">{turn.artifact.code}</pre>\n              }\n              copyText={turn.artifact.code}\n              preview={<SummerBannerPreview />}\n              title={turn.artifact.title}\n            />\n          ) : null}\n\n          {turn.text ? (\n            <AIResponse citations={turn.citations} text={turn.text} />\n          ) : null}\n\n          {turn.sources ? <AISources sources={turn.sources} /> : null}\n        </div>\n      </AIMessage>\n\n      {turn.approval ? (\n        <AIApproval\n          onDecide={() => {\n            // Demo only.\n          }}\n          options={turn.approval.options}\n          question={turn.approval.question}\n        />\n      ) : null}\n\n      {turn.suggestions ? (\n        <AISuggestions\n          label=\"Follow-ups\"\n          onSelect={(suggestion) => onSuggestion(suggestion.label)}\n          suggestions={turn.suggestions}\n        />\n      ) : null}\n    </div>\n  );\n};\n\nconst KIB = 1024;\nconst MIB = KIB * KIB;\n\nconst formatBytes = (size?: number) => {\n  if (!size) {\n    return \"\";\n  }\n  return size >= MIB\n    ? `${(size / MIB).toFixed(1)} MB`\n    : `${Math.round(size / KIB)} KB`;\n};\n\n/**\n * Model picker for the composer.\n *\n * Small enough to keep local: the template should not drag a popover library in\n * for one menu, and the pattern is the same one the sidebar's account menu uses.\n */\nconst ModelPicker = ({\n  onSelect,\n  value,\n}: {\n  onSelect: (label: string) => void;\n  value: string;\n}) => {\n  const [isOpen, setIsOpen] = useState(false);\n  const containerRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    if (!isOpen) {\n      return;\n    }\n    const onPointerDown = (event: PointerEvent) => {\n      if (!containerRef.current?.contains(event.target as Node)) {\n        setIsOpen(false);\n      }\n    };\n    const onKeyDown = (event: KeyboardEvent) => {\n      if (event.key === \"Escape\") {\n        setIsOpen(false);\n      }\n    };\n    document.addEventListener(\"pointerdown\", onPointerDown);\n    document.addEventListener(\"keydown\", onKeyDown);\n    return () => {\n      document.removeEventListener(\"pointerdown\", onPointerDown);\n      document.removeEventListener(\"keydown\", onKeyDown);\n    };\n  }, [isOpen]);\n\n  return (\n    <div className=\"relative\" ref={containerRef}>\n      <button\n        aria-expanded={isOpen}\n        aria-haspopup=\"menu\"\n        className=\"flex items-center gap-1 rounded-lg px-2 py-1.5 text-muted-foreground text-xs transition-colors hover:bg-muted hover:text-foreground\"\n        onClick={() => setIsOpen((open) => !open)}\n        type=\"button\"\n      >\n        {value}\n        <ChevronDown aria-hidden=\"true\" size={12} />\n      </button>\n\n      {isOpen ? (\n        <div\n          className=\"absolute bottom-full left-0 z-10 mb-2 w-52 overflow-hidden rounded-xl border border-border/60 bg-background p-1 shadow-black/10 shadow-lg\"\n          role=\"menu\"\n        >\n          {MODELS.map((option) => (\n            <button\n              className={cn(\n                \"flex w-full flex-col rounded-lg px-2 py-1.5 text-left transition-colors hover:bg-muted\",\n                option.label === value\n                  ? \"text-foreground\"\n                  : \"text-muted-foreground\"\n              )}\n              key={option.id}\n              onClick={() => {\n                onSelect(option.label);\n                setIsOpen(false);\n              }}\n              role=\"menuitem\"\n              type=\"button\"\n            >\n              <span className=\"text-sm\">{option.label}</span>\n              <span className=\"text-muted-foreground text-xs\">\n                {option.note}\n              </span>\n            </button>\n          ))}\n        </div>\n      ) : null}\n    </div>\n  );\n};\n\n/** The artifact's rendered pane — the real banner, not a grey box. */\nconst SummerBannerPreview = () => (\n  <section className=\"rounded-2xl bg-gradient-to-br from-pink-200 to-sky-200 p-6 text-neutral-900\">\n    <p className=\"text-[0.65rem] uppercase tracking-widest\">Summer 2026</p>\n    <h3 className=\"mt-1.5 font-semibold text-2xl\">Two scoops, one price</h3>\n    <p className=\"mt-2 max-w-sm text-sm\">\n      Every weekday before 5pm, all summer.\n    </p>\n  </section>\n);\n\nexport default ChatThread;\n","path":"chat-thread.tsx","target":"components/smoothui/chat-template/chat-thread.tsx","type":"registry:block"},{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useState } from \"react\";\nimport { type ChatConversation, CONVERSATIONS } from \"./chat-data\";\nimport ChatSidebar from \"./chat-sidebar\";\nimport ChatThread from \"./chat-thread\";\n\nconst NEW_CHAT_ID = \"__new__\";\n\n/** No overshoot: a drawer that bounces past its edge reads as a bug. */\nconst DRAWER_SPRING = { bounce: 0, duration: 0.3, type: \"spring\" as const };\nconst SCRIM_FADE = { duration: 0.2 };\n\nexport type ChatTemplateProps = {\n  className?: string;\n  /** Which conversation opens first. Defaults to the newest one. */\n  defaultConversationId?: string;\n  /** Swap in your own transcripts. The shape is `ChatConversation`. */\n  conversations?: ChatConversation[];\n};\n\n/**\n * A full chat surface — sidebar, thread, composer — wired to a fixed script.\n *\n * There is no model behind it and no network call anywhere: every reply is\n * scripted in `chat-data.ts`. Point the composer at your own endpoint and the\n * rest of the template is already the product.\n */\nconst ChatTemplate = ({\n  className,\n  conversations = CONVERSATIONS,\n  defaultConversationId,\n}: ChatTemplateProps) => {\n  const [activeId, setActiveId] = useState(\n    defaultConversationId ?? conversations[0]?.id ?? NEW_CHAT_ID\n  );\n  const [isSidebarOpen, setIsSidebarOpen] = useState(true);\n  // Narrow screens have no column for the list, so it arrives as a drawer.\n  const [isDrawerOpen, setIsDrawerOpen] = useState(false);\n  const shouldReduceMotion = useReducedMotion();\n\n  const open = (id: string) => {\n    setActiveId(id);\n    setIsDrawerOpen(false);\n  };\n\n  const active = conversations.find(\n    (conversation) => conversation.id === activeId\n  );\n\n  return (\n    <div\n      className={cn(\n        \"relative flex h-full min-h-0 w-full overflow-hidden bg-background text-foreground\",\n        className\n      )}\n    >\n      {/* Collapses to an icon rail rather than disappearing, so navigation stays\n          one click away. */}\n      <ChatSidebar\n        activeId={activeId}\n        className=\"hidden md:flex\"\n        collapsed={!isSidebarOpen}\n        conversations={conversations}\n        onNewChat={() => open(NEW_CHAT_ID)}\n        onSelect={open}\n        onToggleCollapsed={() => setIsSidebarOpen((isOpen) => !isOpen)}\n      />\n\n      {/* Below `md` the same list slides over the thread. Without it there is no\n          way to change conversation on a phone, which is most of what a chat\n          app's navigation is for.\n\n          Both children are keyed: `AnimatePresence` tracks its direct children by\n          key, and without one it never runs the enter or the exit — the panel\n          just sat parked off-screen. */}\n      <AnimatePresence>\n        {isDrawerOpen ? (\n          <>\n            <motion.button\n              animate={{ opacity: 1 }}\n              aria-label=\"Close conversations\"\n              className=\"absolute inset-0 z-20 cursor-default bg-foreground/20 md:hidden\"\n              exit={{ opacity: 0 }}\n              initial={{ opacity: 0 }}\n              key=\"chat-drawer-scrim\"\n              onClick={() => setIsDrawerOpen(false)}\n              transition={shouldReduceMotion ? { duration: 0 } : SCRIM_FADE}\n              type=\"button\"\n            />\n            {/* Reduced motion keeps the fade and drops the travel, rather than\n                removing the transition altogether. */}\n            <motion.div\n              animate={{ opacity: 1, x: 0 }}\n              className=\"absolute inset-y-0 left-0 z-30 flex md:hidden\"\n              exit={\n                shouldReduceMotion ? { opacity: 0 } : { opacity: 0, x: \"-100%\" }\n              }\n              initial={\n                shouldReduceMotion ? { opacity: 0 } : { opacity: 1, x: \"-100%\" }\n              }\n              key=\"chat-drawer-panel\"\n              transition={shouldReduceMotion ? SCRIM_FADE : DRAWER_SPRING}\n            >\n              <ChatSidebar\n                activeId={activeId}\n                className=\"h-full bg-background shadow-black/10 shadow-xl\"\n                collapsed={false}\n                conversations={conversations}\n                onNewChat={() => open(NEW_CHAT_ID)}\n                onSelect={open}\n                onToggleCollapsed={() => setIsDrawerOpen(false)}\n              />\n            </motion.div>\n          </>\n        ) : null}\n      </AnimatePresence>\n\n      <ChatThread\n        key={activeId}\n        onOpenSidebar={() => setIsDrawerOpen(true)}\n        title={active?.title ?? \"New chat\"}\n        turns={active?.turns ?? []}\n      />\n    </div>\n  );\n};\n\nexport default ChatTemplate;\nexport type { ChatConversation, ChatTurn } from \"./chat-data\";\n","path":"index.tsx","target":"components/smoothui/chat-template/index.tsx","type":"registry:block"}],"name":"chat-template","registryDependencies":["https://smoothui.dev/r/ai-approval.json","https://smoothui.dev/r/ai-artifact.json","https://smoothui.dev/r/ai-context-meter.json","https://smoothui.dev/r/ai-conversation.json","https://smoothui.dev/r/ai-core.json","https://smoothui.dev/r/ai-diff.json","https://smoothui.dev/r/ai-loader.json","https://smoothui.dev/r/ai-message.json","https://smoothui.dev/r/ai-prompt-input.json","https://smoothui.dev/r/ai-reasoning.json","https://smoothui.dev/r/ai-response.json","https://smoothui.dev/r/ai-sources.json","https://smoothui.dev/r/ai-suggestions.json","https://smoothui.dev/r/ai-task-list.json","https://smoothui.dev/r/ai-tool-call.json","https://smoothui.dev/r/siri-orb.json"],"title":"Chat Template","type":"registry:block"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","cssVars":{"dark":{"accent":"oklch(0.72 0.2 352.53)","accent-foreground":"oklch(1 0 0)","background":"oklch(0.2 0 0)","border":"oklch(0.315 0 0)","card":"oklch(0.2 0 0)","card-foreground":"oklch(0.96 0 0)","chart-1":"oklch(0.72 0.2 352.53)","chart-2":"oklch(0.66 0.21 354.31)","chart-3":"oklch(0.66 0 0)","chart-4":"oklch(0.47 0 0)","chart-5":"oklch(0.36 0 0)","destructive":"oklch(0.704 0.191 22.216)","foreground":"oklch(0.96 0 0)","input":"oklch(0.55 0 0)","muted":"oklch(0.275 0 0)","muted-foreground":"oklch(0.66 0 0)","popover":"oklch(0.2 0 0)","popover-foreground":"oklch(0.96 0 0)","primary":"oklch(0.235 0 0)","primary-foreground":"oklch(0.85 0 0)","ring":"oklch(0.72 0.2 352.53)","secondary":"oklch(0.275 0 0)","secondary-foreground":"oklch(0.74 0 0)","sidebar":"oklch(0.235 0 0)","sidebar-accent":"oklch(0.275 0 0)","sidebar-accent-foreground":"oklch(0.74 0 0)","sidebar-border":"oklch(0.36 0 0)","sidebar-foreground":"oklch(0.96 0 0)","sidebar-primary":"oklch(0.72 0.2 352.53)","sidebar-primary-foreground":"oklch(1 0 0)","sidebar-ring":"oklch(0.72 0.2 352.53)"},"light":{"accent":"oklch(0.72 0.2 352.53)","accent-foreground":"oklch(1 0 0)","background":"oklch(0.99 0 0)","border":"oklch(0.91 0 0)","card":"oklch(0.99 0 0)","card-foreground":"oklch(0.22 0 0)","chart-1":"oklch(0.72 0.2 352.53)","chart-2":"oklch(0.66 0.21 354.31)","chart-3":"oklch(0.56 0 0)","chart-4":"oklch(0.74 0 0)","chart-5":"oklch(0.865 0 0)","destructive":"oklch(0.577 0.245 27.325)","foreground":"oklch(0.22 0 0)","input":"oklch(0.66 0 0)","muted":"oklch(0.945 0 0)","muted-foreground":"oklch(0.56 0 0)","popover":"oklch(0.99 0 0)","popover-foreground":"oklch(0.22 0 0)","primary":"oklch(0.972 0 0)","primary-foreground":"oklch(0.36 0 0)","ring":"oklch(0.72 0.2 352.53)","secondary":"oklch(0.945 0 0)","secondary-foreground":"oklch(0.47 0 0)","sidebar":"oklch(0.972 0 0)","sidebar-accent":"oklch(0.945 0 0)","sidebar-accent-foreground":"oklch(0.47 0 0)","sidebar-border":"oklch(0.865 0 0)","sidebar-foreground":"oklch(0.22 0 0)","sidebar-primary":"oklch(0.72 0.2 352.53)","sidebar-primary-foreground":"oklch(1 0 0)","sidebar-ring":"oklch(0.72 0.2 352.53)","radius":"0.625rem"},"theme":{"font-sans":"Inter, sans-serif"}},"description":"SmoothUI Candy theme: smooth neutral scale with a candy accent, light and dark mode included.","name":"theme-candy","title":"SmoothUI Candy","type":"registry:theme"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","cssVars":{"dark":{"accent":"oklch(0.65 0.22 300.21)","accent-foreground":"oklch(1 0 0)","background":"oklch(0.2 0 0)","border":"oklch(0.315 0 0)","card":"oklch(0.2 0 0)","card-foreground":"oklch(0.96 0 0)","chart-1":"oklch(0.65 0.22 300.21)","chart-2":"oklch(0.54 0.23 286.53)","chart-3":"oklch(0.66 0 0)","chart-4":"oklch(0.47 0 0)","chart-5":"oklch(0.36 0 0)","destructive":"oklch(0.704 0.191 22.216)","foreground":"oklch(0.96 0 0)","input":"oklch(0.55 0 0)","muted":"oklch(0.275 0 0)","muted-foreground":"oklch(0.66 0 0)","popover":"oklch(0.2 0 0)","popover-foreground":"oklch(0.96 0 0)","primary":"oklch(0.235 0 0)","primary-foreground":"oklch(0.85 0 0)","ring":"oklch(0.65 0.22 300.21)","secondary":"oklch(0.275 0 0)","secondary-foreground":"oklch(0.74 0 0)","sidebar":"oklch(0.235 0 0)","sidebar-accent":"oklch(0.275 0 0)","sidebar-accent-foreground":"oklch(0.74 0 0)","sidebar-border":"oklch(0.36 0 0)","sidebar-foreground":"oklch(0.96 0 0)","sidebar-primary":"oklch(0.65 0.22 300.21)","sidebar-primary-foreground":"oklch(1 0 0)","sidebar-ring":"oklch(0.65 0.22 300.21)"},"light":{"accent":"oklch(0.65 0.22 300.21)","accent-foreground":"oklch(1 0 0)","background":"oklch(0.99 0 0)","border":"oklch(0.91 0 0)","card":"oklch(0.99 0 0)","card-foreground":"oklch(0.22 0 0)","chart-1":"oklch(0.65 0.22 300.21)","chart-2":"oklch(0.54 0.23 286.53)","chart-3":"oklch(0.56 0 0)","chart-4":"oklch(0.74 0 0)","chart-5":"oklch(0.865 0 0)","destructive":"oklch(0.577 0.245 27.325)","foreground":"oklch(0.22 0 0)","input":"oklch(0.66 0 0)","muted":"oklch(0.945 0 0)","muted-foreground":"oklch(0.56 0 0)","popover":"oklch(0.99 0 0)","popover-foreground":"oklch(0.22 0 0)","primary":"oklch(0.972 0 0)","primary-foreground":"oklch(0.36 0 0)","ring":"oklch(0.65 0.22 300.21)","secondary":"oklch(0.945 0 0)","secondary-foreground":"oklch(0.47 0 0)","sidebar":"oklch(0.972 0 0)","sidebar-accent":"oklch(0.945 0 0)","sidebar-accent-foreground":"oklch(0.47 0 0)","sidebar-border":"oklch(0.865 0 0)","sidebar-foreground":"oklch(0.22 0 0)","sidebar-primary":"oklch(0.65 0.22 300.21)","sidebar-primary-foreground":"oklch(1 0 0)","sidebar-ring":"oklch(0.65 0.22 300.21)","radius":"0.625rem"},"theme":{"font-sans":"Inter, sans-serif"}},"description":"SmoothUI Indigo theme: smooth neutral scale with a indigo accent, light and dark mode included.","name":"theme-indigo","title":"SmoothUI Indigo","type":"registry:theme"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","cssVars":{"dark":{"accent":"oklch(0.67 0.17 257.78)","accent-foreground":"oklch(1 0 0)","background":"oklch(0.2 0 0)","border":"oklch(0.315 0 0)","card":"oklch(0.2 0 0)","card-foreground":"oklch(0.96 0 0)","chart-1":"oklch(0.67 0.17 257.78)","chart-2":"oklch(0.59 0.21 258.02)","chart-3":"oklch(0.66 0 0)","chart-4":"oklch(0.47 0 0)","chart-5":"oklch(0.36 0 0)","destructive":"oklch(0.704 0.191 22.216)","foreground":"oklch(0.96 0 0)","input":"oklch(0.55 0 0)","muted":"oklch(0.275 0 0)","muted-foreground":"oklch(0.66 0 0)","popover":"oklch(0.2 0 0)","popover-foreground":"oklch(0.96 0 0)","primary":"oklch(0.235 0 0)","primary-foreground":"oklch(0.85 0 0)","ring":"oklch(0.67 0.17 257.78)","secondary":"oklch(0.275 0 0)","secondary-foreground":"oklch(0.74 0 0)","sidebar":"oklch(0.235 0 0)","sidebar-accent":"oklch(0.275 0 0)","sidebar-accent-foreground":"oklch(0.74 0 0)","sidebar-border":"oklch(0.36 0 0)","sidebar-foreground":"oklch(0.96 0 0)","sidebar-primary":"oklch(0.67 0.17 257.78)","sidebar-primary-foreground":"oklch(1 0 0)","sidebar-ring":"oklch(0.67 0.17 257.78)"},"light":{"accent":"oklch(0.67 0.17 257.78)","accent-foreground":"oklch(1 0 0)","background":"oklch(0.99 0 0)","border":"oklch(0.91 0 0)","card":"oklch(0.99 0 0)","card-foreground":"oklch(0.22 0 0)","chart-1":"oklch(0.67 0.17 257.78)","chart-2":"oklch(0.59 0.21 258.02)","chart-3":"oklch(0.56 0 0)","chart-4":"oklch(0.74 0 0)","chart-5":"oklch(0.865 0 0)","destructive":"oklch(0.577 0.245 27.325)","foreground":"oklch(0.22 0 0)","input":"oklch(0.66 0 0)","muted":"oklch(0.945 0 0)","muted-foreground":"oklch(0.56 0 0)","popover":"oklch(0.99 0 0)","popover-foreground":"oklch(0.22 0 0)","primary":"oklch(0.972 0 0)","primary-foreground":"oklch(0.36 0 0)","ring":"oklch(0.67 0.17 257.78)","secondary":"oklch(0.945 0 0)","secondary-foreground":"oklch(0.47 0 0)","sidebar":"oklch(0.972 0 0)","sidebar-accent":"oklch(0.945 0 0)","sidebar-accent-foreground":"oklch(0.47 0 0)","sidebar-border":"oklch(0.865 0 0)","sidebar-foreground":"oklch(0.22 0 0)","sidebar-primary":"oklch(0.67 0.17 257.78)","sidebar-primary-foreground":"oklch(1 0 0)","sidebar-ring":"oklch(0.67 0.17 257.78)","radius":"0.625rem"},"theme":{"font-sans":"Inter, sans-serif"}},"description":"SmoothUI Blue theme: smooth neutral scale with a blue accent, light and dark mode included.","name":"theme-blue","title":"SmoothUI Blue","type":"registry:theme"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","cssVars":{"dark":{"accent":"oklch(0.67 0.21 24.28)","accent-foreground":"oklch(1 0 0)","background":"oklch(0.2 0 0)","border":"oklch(0.315 0 0)","card":"oklch(0.2 0 0)","card-foreground":"oklch(0.96 0 0)","chart-1":"oklch(0.67 0.21 24.28)","chart-2":"oklch(0.62 0.25 28.23)","chart-3":"oklch(0.66 0 0)","chart-4":"oklch(0.47 0 0)","chart-5":"oklch(0.36 0 0)","destructive":"oklch(0.704 0.191 22.216)","foreground":"oklch(0.96 0 0)","input":"oklch(0.55 0 0)","muted":"oklch(0.275 0 0)","muted-foreground":"oklch(0.66 0 0)","popover":"oklch(0.2 0 0)","popover-foreground":"oklch(0.96 0 0)","primary":"oklch(0.235 0 0)","primary-foreground":"oklch(0.85 0 0)","ring":"oklch(0.67 0.21 24.28)","secondary":"oklch(0.275 0 0)","secondary-foreground":"oklch(0.74 0 0)","sidebar":"oklch(0.235 0 0)","sidebar-accent":"oklch(0.275 0 0)","sidebar-accent-foreground":"oklch(0.74 0 0)","sidebar-border":"oklch(0.36 0 0)","sidebar-foreground":"oklch(0.96 0 0)","sidebar-primary":"oklch(0.67 0.21 24.28)","sidebar-primary-foreground":"oklch(1 0 0)","sidebar-ring":"oklch(0.67 0.21 24.28)"},"light":{"accent":"oklch(0.67 0.21 24.28)","accent-foreground":"oklch(1 0 0)","background":"oklch(0.99 0 0)","border":"oklch(0.91 0 0)","card":"oklch(0.99 0 0)","card-foreground":"oklch(0.22 0 0)","chart-1":"oklch(0.67 0.21 24.28)","chart-2":"oklch(0.62 0.25 28.23)","chart-3":"oklch(0.56 0 0)","chart-4":"oklch(0.74 0 0)","chart-5":"oklch(0.865 0 0)","destructive":"oklch(0.577 0.245 27.325)","foreground":"oklch(0.22 0 0)","input":"oklch(0.66 0 0)","muted":"oklch(0.945 0 0)","muted-foreground":"oklch(0.56 0 0)","popover":"oklch(0.99 0 0)","popover-foreground":"oklch(0.22 0 0)","primary":"oklch(0.972 0 0)","primary-foreground":"oklch(0.36 0 0)","ring":"oklch(0.67 0.21 24.28)","secondary":"oklch(0.945 0 0)","secondary-foreground":"oklch(0.47 0 0)","sidebar":"oklch(0.972 0 0)","sidebar-accent":"oklch(0.945 0 0)","sidebar-accent-foreground":"oklch(0.47 0 0)","sidebar-border":"oklch(0.865 0 0)","sidebar-foreground":"oklch(0.22 0 0)","sidebar-primary":"oklch(0.67 0.21 24.28)","sidebar-primary-foreground":"oklch(1 0 0)","sidebar-ring":"oklch(0.67 0.21 24.28)","radius":"0.625rem"},"theme":{"font-sans":"Inter, sans-serif"}},"description":"SmoothUI Red theme: smooth neutral scale with a red accent, light and dark mode included.","name":"theme-red","title":"SmoothUI Red","type":"registry:theme"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","cssVars":{"dark":{"accent":"oklch(0.75 0.17 47.65)","accent-foreground":"oklch(1 0 0)","background":"oklch(0.2 0 0)","border":"oklch(0.315 0 0)","card":"oklch(0.2 0 0)","card-foreground":"oklch(0.96 0 0)","chart-1":"oklch(0.75 0.17 47.65)","chart-2":"oklch(0.68 0.21 40.59)","chart-3":"oklch(0.66 0 0)","chart-4":"oklch(0.47 0 0)","chart-5":"oklch(0.36 0 0)","destructive":"oklch(0.704 0.191 22.216)","foreground":"oklch(0.96 0 0)","input":"oklch(0.55 0 0)","muted":"oklch(0.275 0 0)","muted-foreground":"oklch(0.66 0 0)","popover":"oklch(0.2 0 0)","popover-foreground":"oklch(0.96 0 0)","primary":"oklch(0.235 0 0)","primary-foreground":"oklch(0.85 0 0)","ring":"oklch(0.75 0.17 47.65)","secondary":"oklch(0.275 0 0)","secondary-foreground":"oklch(0.74 0 0)","sidebar":"oklch(0.235 0 0)","sidebar-accent":"oklch(0.275 0 0)","sidebar-accent-foreground":"oklch(0.74 0 0)","sidebar-border":"oklch(0.36 0 0)","sidebar-foreground":"oklch(0.96 0 0)","sidebar-primary":"oklch(0.75 0.17 47.65)","sidebar-primary-foreground":"oklch(1 0 0)","sidebar-ring":"oklch(0.75 0.17 47.65)"},"light":{"accent":"oklch(0.75 0.17 47.65)","accent-foreground":"oklch(1 0 0)","background":"oklch(0.99 0 0)","border":"oklch(0.91 0 0)","card":"oklch(0.99 0 0)","card-foreground":"oklch(0.22 0 0)","chart-1":"oklch(0.75 0.17 47.65)","chart-2":"oklch(0.68 0.21 40.59)","chart-3":"oklch(0.56 0 0)","chart-4":"oklch(0.74 0 0)","chart-5":"oklch(0.865 0 0)","destructive":"oklch(0.577 0.245 27.325)","foreground":"oklch(0.22 0 0)","input":"oklch(0.66 0 0)","muted":"oklch(0.945 0 0)","muted-foreground":"oklch(0.56 0 0)","popover":"oklch(0.99 0 0)","popover-foreground":"oklch(0.22 0 0)","primary":"oklch(0.972 0 0)","primary-foreground":"oklch(0.36 0 0)","ring":"oklch(0.75 0.17 47.65)","secondary":"oklch(0.945 0 0)","secondary-foreground":"oklch(0.47 0 0)","sidebar":"oklch(0.972 0 0)","sidebar-accent":"oklch(0.945 0 0)","sidebar-accent-foreground":"oklch(0.47 0 0)","sidebar-border":"oklch(0.865 0 0)","sidebar-foreground":"oklch(0.22 0 0)","sidebar-primary":"oklch(0.75 0.17 47.65)","sidebar-primary-foreground":"oklch(1 0 0)","sidebar-ring":"oklch(0.75 0.17 47.65)","radius":"0.625rem"},"theme":{"font-sans":"Inter, sans-serif"}},"description":"SmoothUI Orange theme: smooth neutral scale with a orange accent, light and dark mode included.","name":"theme-orange","title":"SmoothUI Orange","type":"registry:theme"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","cssVars":{"dark":{"accent":"oklch(0.70 0.15 162.48)","accent-foreground":"oklch(1 0 0)","background":"oklch(0.2 0 0)","border":"oklch(0.315 0 0)","card":"oklch(0.2 0 0)","card-foreground":"oklch(0.96 0 0)","chart-1":"oklch(0.70 0.15 162.48)","chart-2":"oklch(0.60 0.13 163.23)","chart-3":"oklch(0.66 0 0)","chart-4":"oklch(0.47 0 0)","chart-5":"oklch(0.36 0 0)","destructive":"oklch(0.704 0.191 22.216)","foreground":"oklch(0.96 0 0)","input":"oklch(0.55 0 0)","muted":"oklch(0.275 0 0)","muted-foreground":"oklch(0.66 0 0)","popover":"oklch(0.2 0 0)","popover-foreground":"oklch(0.96 0 0)","primary":"oklch(0.235 0 0)","primary-foreground":"oklch(0.85 0 0)","ring":"oklch(0.70 0.15 162.48)","secondary":"oklch(0.275 0 0)","secondary-foreground":"oklch(0.74 0 0)","sidebar":"oklch(0.235 0 0)","sidebar-accent":"oklch(0.275 0 0)","sidebar-accent-foreground":"oklch(0.74 0 0)","sidebar-border":"oklch(0.36 0 0)","sidebar-foreground":"oklch(0.96 0 0)","sidebar-primary":"oklch(0.70 0.15 162.48)","sidebar-primary-foreground":"oklch(1 0 0)","sidebar-ring":"oklch(0.70 0.15 162.48)"},"light":{"accent":"oklch(0.70 0.15 162.48)","accent-foreground":"oklch(1 0 0)","background":"oklch(0.99 0 0)","border":"oklch(0.91 0 0)","card":"oklch(0.99 0 0)","card-foreground":"oklch(0.22 0 0)","chart-1":"oklch(0.70 0.15 162.48)","chart-2":"oklch(0.60 0.13 163.23)","chart-3":"oklch(0.56 0 0)","chart-4":"oklch(0.74 0 0)","chart-5":"oklch(0.865 0 0)","destructive":"oklch(0.577 0.245 27.325)","foreground":"oklch(0.22 0 0)","input":"oklch(0.66 0 0)","muted":"oklch(0.945 0 0)","muted-foreground":"oklch(0.56 0 0)","popover":"oklch(0.99 0 0)","popover-foreground":"oklch(0.22 0 0)","primary":"oklch(0.972 0 0)","primary-foreground":"oklch(0.36 0 0)","ring":"oklch(0.70 0.15 162.48)","secondary":"oklch(0.945 0 0)","secondary-foreground":"oklch(0.47 0 0)","sidebar":"oklch(0.972 0 0)","sidebar-accent":"oklch(0.945 0 0)","sidebar-accent-foreground":"oklch(0.47 0 0)","sidebar-border":"oklch(0.865 0 0)","sidebar-foreground":"oklch(0.22 0 0)","sidebar-primary":"oklch(0.70 0.15 162.48)","sidebar-primary-foreground":"oklch(1 0 0)","sidebar-ring":"oklch(0.70 0.15 162.48)","radius":"0.625rem"},"theme":{"font-sans":"Inter, sans-serif"}},"description":"SmoothUI Green theme: smooth neutral scale with a green accent, light and dark mode included.","name":"theme-green","title":"SmoothUI Green","type":"registry:theme"},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","description":"Teaches AI coding assistants how to install SmoothUI components, blocks, and themes, and how to follow SmoothUI animation conventions.","files":[{"content":"---\nname: smoothui\ndescription: Install and use SmoothUI — animated React components and blocks built with Tailwind CSS v4 and Motion, distributed through a shadcn-compatible registry. Use when adding animated UI components, page blocks, or SmoothUI themes to a project.\n---\n\n# SmoothUI\n\nSmoothUI is a registry of animated React components (buttons, inputs, cards, AI chat primitives) and page blocks (heroes, pricing, testimonials, FAQs, CTAs) built with React 19, Tailwind CSS v4, and Motion (Framer Motion). Docs: https://smoothui.dev\n\n## Installing components\n\nEvery item installs through the shadcn CLI. Browse names at https://smoothui.dev/doc or fetch the machine-readable index at https://smoothui.dev/r/registry.json\n\n```bash\n# Single component\nnpx shadcn@latest add https://smoothui.dev/r/smooth-button.json\n\n# Page block (brings its dependencies automatically)\nnpx shadcn@latest add https://smoothui.dev/r/pricing-1.json\n```\n\nTo enable the short `@smoothui/<name>` syntax, add the namespace to `components.json`:\n\n```json\n{\n  \"registries\": {\n    \"@smoothui\": \"https://smoothui.dev/r/{name}.json\"\n  }\n}\n```\n\nThen: `npx shadcn@latest add @smoothui/smooth-button`\n\n## Where files land\n\n- Components and blocks: `components/smoothui/<name>/`\n- Shared block helpers: `components/smoothui/shared/`\n- Animation constants: `components/smoothui/lib/animation.ts`\n- Demo data helpers: `lib/smoothui-data/`\n\nImports inside installed files use the `@/` alias and resolve out of the box in any shadcn-initialized project.\n\n## Themes\n\nSmoothUI ships installable color themes (light + dark, standard shadcn CSS variables, compatible with Radix UI and Base UI projects):\n\n```bash\nnpx shadcn@latest add https://smoothui.dev/r/theme-candy.json\n# Also: theme-indigo, theme-blue, theme-red, theme-orange, theme-green\n```\n\n## Animation conventions (follow when extending SmoothUI components)\n\n- Animate only `transform` and `opacity`; durations 0.2–0.3s for UI.\n- Spring animations: `{ type: \"spring\", duration: 0.25, bounce: 0.1 }`; bounce above 0.1 only for playful or drag interactions.\n- Easing: `cubic-bezier` values, never string easing like `\"easeInOut\"`. Entering elements: `cubic-bezier(.23, 1, .32, 1)`.\n- Accessibility is mandatory: import `useReducedMotion` from `motion/react`, and disable or reduce animations when it returns true.\n- Hover-driven animation needs hover-capable device detection: `window.matchMedia(\"(hover: hover) and (pointer: fine)\")`.\n\n## AI-friendly endpoints\n\n- `https://smoothui.dev/llms.txt` — index for LLMs\n- `https://smoothui.dev/llms-full.txt` — full documentation dump\n- `https://smoothui.dev/llms-components.json` — structured component metadata\n","path":"SKILL.md","target":".claude/skills/smoothui/SKILL.md","type":"registry:file"}],"name":"skill","title":"SmoothUI Skill","type":"registry:item"}],"name":"SmoothUI Registry"}