{"name":"SmoothUI Registry","homepage":"https://smoothui.dev/","items":[{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"cta-1","type":"registry:ui","title":"cta-1","description":"Centered CTA block with radial glow background","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion"],"devDependencies":[],"registryDependencies":["https://smoothui.dev/r/smooth-button.json"],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport SmoothButton from \"@repo/smoothui/components/smooth-button\";\nimport { motion, useReducedMotion } from \"motion/react\";\n\nconst SPRING = {\n  type: \"spring\" as const,\n  duration: 0.25,\n  bounce: 0.1,\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","target":"components/smoothui/cta-1/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"cta-2","type":"registry:ui","title":"cta-2","description":"Split layout CTA block with text and illustration","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["@smoothui/data","motion"],"devDependencies":[],"registryDependencies":["https://smoothui.dev/r/smooth-button.json"],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport SmoothButton from \"@repo/smoothui/components/smooth-button\";\nimport { getImageKitUrl } from \"@smoothui/data\";\nimport { motion, useReducedMotion } from \"motion/react\";\n\nconst SPRING = {\n  type: \"spring\" as const,\n  duration: 0.25,\n  bounce: 0.1,\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                src={getImageKitUrl(\"/images/hero-smoothui.png\", {\n                  width: 800,\n                  quality: 85,\n                  format: \"auto\",\n                })}\n              />\n            </div>\n          </motion.div>\n        </div>\n      </div>\n    </section>\n  );\n}\n\nexport default CtaSplit;\n","target":"components/smoothui/cta-2/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"cta-3","type":"registry:ui","title":"cta-3","description":"Compact banner CTA block","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion"],"devDependencies":[],"registryDependencies":["https://smoothui.dev/r/smooth-button.json"],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport SmoothButton from \"@repo/smoothui/components/smooth-button\";\nimport { motion, useReducedMotion } from \"motion/react\";\n\nconst SPRING = {\n  type: \"spring\" as const,\n  duration: 0.25,\n  bounce: 0.1,\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","target":"components/smoothui/cta-3/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"faq-1","type":"registry:ui","title":"faq-1","description":"FAQ grid block with categorized tabs and icons","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["lucide-react","motion"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","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 } 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  Clock,\n  WalletCards,\n  ShoppingBag,\n  Globe,\n  Scale,\n  Activity,\n  DollarSign,\n  Lock,\n  Undo2,\n  Download,\n  Settings,\n  HelpCircle,\n  MessageCircle,\n  Users,\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      name: \"General\",\n      id: \"general\",\n      faqs: [\n        {\n          question: \"How do I install Smoothui?\",\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        },\n        {\n          question: \"What are the system requirements?\",\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        },\n        {\n          question: \"Is Smoothui free to use?\",\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        },\n      ],\n    },\n    {\n      name: \"Components\",\n      id: \"components\",\n      faqs: [\n        {\n          question: \"How many components are included?\",\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        },\n        {\n          question: \"Can I customize the styling?\",\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        },\n        {\n          question: \"Are the components accessible?\",\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        },\n      ],\n    },\n    {\n      name: \"Support\",\n      id: \"support\",\n      faqs: [\n        {\n          question: \"How can I get help?\",\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        },\n        {\n          question: \"Do you offer custom development?\",\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        },\n        {\n          question: \"What's your response time?\",\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        },\n      ],\n    },\n  ],\n}: FaqsGridProps) {\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 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={{ scale: HOVER_SCALE }}\n                whileTap={{ 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=\"activeTab\"\n                    transition={{\n                      type: \"spring\" as const,\n                      stiffness: SPRING_STIFFNESS,\n                      damping: SPRING_DAMPING,\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={{ opacity: 1, y: 0 }}\n              exit={{ opacity: 0, y: EXIT_Y_OFFSET }}\n              initial={{ opacity: 0, y: INITIAL_Y_OFFSET }}\n              key={activeTab}\n              transition={{ duration: ANIMATION_DURATION, ease: \"easeInOut\" }}\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={{ opacity: 1, y: 0 }}\n                        className=\"space-y-3\"\n                        initial={{ opacity: 0, y: INITIAL_Y_OFFSET }}\n                        key={`${categories[activeTab].id}-${faqIndex}`}\n                        transition={{\n                          duration: 0.4,\n                          delay: faqIndex * FAQ_STAGGER_DELAY,\n                        }}\n                      >\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                        <dt className=\"font-semibold text-foreground\">\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","target":"components/smoothui/faq-1/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"faq-2","type":"registry:ui","title":"faq-2","description":"FAQ accordion block with expandable questions","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport { AnimatePresence, motion } 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      question: \"What is Smoothui?\",\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    },\n    {\n      question: \"How do I get started?\",\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    },\n    {\n      question: \"Is it free to use?\",\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    },\n    {\n      question: \"Can I customize the components?\",\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    },\n    {\n      question: \"What frameworks are supported?\",\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    },\n    {\n      question: \"How often do you release updates?\",\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    },\n  ],\n}: FaqsAccordionProps) {\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={{ opacity: 1, y: 0 }}\n          className=\"mb-16 text-center\"\n          initial={{ opacity: 0, y: INITIAL_Y_OFFSET }}\n          transition={{ duration: ANIMATION_DURATION }}\n          viewport={{ once: true }}\n          whileInView={{ opacity: 1, y: 0 }}\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={{ opacity: 1, y: 0 }}\n              className=\"group overflow-hidden rounded-2xl border border-border bg-background transition-all hover:border-brand hover:shadow-lg\"\n              initial={{ opacity: 0, y: INITIAL_Y_OFFSET }}\n              key={faq.question}\n              transition={{\n                duration: ANIMATION_DURATION,\n                delay: index * STAGGER_DELAY,\n              }}\n              viewport={{ once: true }}\n              whileInView={{ opacity: 1, y: 0 }}\n            >\n              <motion.button\n                className=\"flex w-full items-center justify-between p-6 text-left transition-colors hover:bg-background/50\"\n                onClick={() => toggleAccordion(index)}\n                type=\"button\"\n                whileHover={{ scale: HOVER_SCALE }}\n                whileTap={{ 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                    rotate:\n                      openIndex === index ? ROTATION_OPEN : ROTATION_CLOSED,\n                  }}\n                  className=\"flex-shrink-0\"\n                  transition={{\n                    duration: ANIMATION_DURATION,\n                    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={{ height: 0, opacity: 0 }}\n                    initial={{ height: 0, opacity: 0 }}\n                    transition={{\n                      duration: ANIMATION_DURATION,\n                      ease: \"easeInOut\",\n                    }}\n                  >\n                    <motion.div\n                      animate={{ y: 0 }}\n                      className=\"px-6 pb-6\"\n                      exit={{ y: INITIAL_CONTENT_Y }}\n                      initial={{ y: INITIAL_CONTENT_Y }}\n                      transition={{\n                        duration: ANIMATION_DURATION,\n                        delay: CONTENT_DELAY,\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","target":"components/smoothui/faq-2/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"faq-3","type":"registry:ui","title":"faq-3","description":"Searchable FAQ section with filter animations","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["lucide-react","motion"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","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    question: \"How do I get started with SmoothUI?\",\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  },\n  {\n    question: \"Is SmoothUI free to use?\",\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  },\n  {\n    question: \"What are the system requirements?\",\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  },\n  {\n    question: \"Can I customize the animations?\",\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  },\n  {\n    question: \"How do I report bugs or issues?\",\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  },\n  {\n    question: \"Is there enterprise support available?\",\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  },\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    : { type: \"spring\" as const, duration: 0.25, bounce: 0.05 };\n\n  const contentTransition = shouldReduceMotion\n    ? { duration: 0 }\n    : { type: \"spring\" as const, duration: 0.25, bounce: 0 };\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, y: 0, scale: 1 }\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, y: 20, scale: 0.95 }\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 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","target":"components/smoothui/faq-3/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"faq-4","type":"registry:ui","title":"faq-4","description":"Categorized FAQ section with tab navigation","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["lucide-react","motion"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","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    name: \"Getting Started\",\n    faqs: [\n      {\n        question: \"How do I set up SmoothUI in my project?\",\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      },\n      {\n        question: \"What are the minimum requirements?\",\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      },\n      {\n        question: \"How do I add my first component?\",\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      },\n    ],\n  },\n  {\n    name: \"Billing\",\n    faqs: [\n      {\n        question: \"Is SmoothUI free to use?\",\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      },\n      {\n        question: \"Do you offer refunds?\",\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      },\n      {\n        question: \"What payment methods do you accept?\",\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      },\n    ],\n  },\n  {\n    name: \"Technical\",\n    faqs: [\n      {\n        question: \"Which browsers are supported?\",\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      },\n      {\n        question: \"How does SmoothUI affect performance?\",\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      },\n      {\n        question: \"Is TypeScript supported?\",\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      },\n      {\n        question: \"Can I customize component styles?\",\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      },\n    ],\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    : { type: \"spring\" as const, duration: 0.25, bounce: 0.05 };\n\n  const contentTransition = shouldReduceMotion\n    ? { duration: 0 }\n    : { type: \"spring\" as const, duration: 0.25, bounce: 0 };\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 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","target":"components/smoothui/faq-4/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"features-1","type":"registry:ui","title":"features-1","description":"Feature grid block with animated cards","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport { cn } from \"@repo/shadcn-ui/lib/utils\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { useEffect, useState } from \"react\";\n\nconst SPRING = {\n  type: \"spring\" as const,\n  duration: 0.25,\n  bounce: 0.1,\n};\n\nconst features = [\n  {\n    title: \"Lightning Fast\",\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  },\n  {\n    title: \"Accessible\",\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  },\n  {\n    title: \"Customizable\",\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  },\n  {\n    title: \"TypeScript First\",\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  },\n  {\n    title: \"Animated\",\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  },\n  {\n    title: \"Open Source\",\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  },\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={{ once: true, margin: \"-100px\" }}\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-primary/10 p-2.5 text-primary\">\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","target":"components/smoothui/features-1/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"features-2","type":"registry:ui","title":"features-2","description":"Bento grid features block with asymmetric layout","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport { cn } from \"@repo/shadcn-ui/lib/utils\";\nimport { motion, useReducedMotion } from \"motion/react\";\n\nconst SPRING = {\n  type: \"spring\" as const,\n  duration: 0.25,\n  bounce: 0.1,\n};\n\nconst features = [\n  {\n    title: \"Smart Analytics\",\n    description:\n      \"Real-time analytics dashboard with customizable metrics and beautiful visualizations.\",\n    span: \"col-span-2 row-span-2\",\n    accent: true,\n  },\n  {\n    title: \"Team Collaboration\",\n    description:\n      \"Work together in real-time with built-in commenting and sharing.\",\n    span: \"col-span-1\",\n  },\n  {\n    title: \"API First\",\n    description: \"RESTful API with comprehensive documentation and SDKs.\",\n    span: \"col-span-1\",\n  },\n  {\n    title: \"Global CDN\",\n    description: \"Lightning-fast delivery from edge locations worldwide.\",\n    span: \"col-span-1\",\n  },\n  {\n    title: \"Security\",\n    description:\n      \"Enterprise-grade security with SOC 2 compliance and encryption.\",\n    span: \"col-span-1\",\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 rounded-xl border p-6\",\n                  feature.accent\n                    ? \"bg-primary/5 ring-1 ring-primary/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={{ once: true, margin: \"-100px\" }}\n                whileInView={\n                  shouldReduceMotion ? { opacity: 1 } : { opacity: 1, scale: 1 }\n                }\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","target":"components/smoothui/features-2/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"features-3","type":"registry:ui","title":"features-3","description":"Alternating features block with side-by-side layout","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["@smoothui/data","motion"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport { cn } from \"@repo/shadcn-ui/lib/utils\";\nimport { getImageKitUrl } from \"@smoothui/data\";\nimport { motion, useReducedMotion } from \"motion/react\";\n\nconst SPRING = {\n  type: \"spring\" as const,\n  duration: 0.25,\n  bounce: 0.1,\n};\n\nconst features = [\n  {\n    title: \"Intuitive Design\",\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      width: 800,\n      quality: 85,\n      format: \"auto\",\n    }),\n    imageAlt: \"Designer working on user interface\",\n  },\n  {\n    title: \"Blazing Performance\",\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      width: 800,\n      quality: 85,\n      format: \"auto\",\n    }),\n    imageAlt: \"SmoothUI component showcase\",\n  },\n  {\n    title: \"Developer Experience\",\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      width: 800,\n      quality: 85,\n      format: \"auto\",\n    }),\n    imageAlt: \"Code example with TypeScript support\",\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={{ once: true, margin: \"-100px\" }}\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={{ once: true, margin: \"-100px\" }}\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                        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","target":"components/smoothui/features-3/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"footer-1","type":"registry:ui","title":"footer-1","description":"Simple footer block with links and social icons","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport { motion } 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    product: [\n      { name: \"Features\", url: \"#features\" },\n      { name: \"Pricing\", url: \"#pricing\" },\n      { name: \"Documentation\", url: \"#docs\" },\n      { name: \"API\", url: \"#api\" },\n    ],\n    company: [\n      { name: \"About\", url: \"#about\" },\n      { name: \"Blog\", url: \"#blog\" },\n      { name: \"Careers\", url: \"#careers\" },\n      { name: \"Contact\", url: \"#contact\" },\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    twitter: \"https://twitter.com\",\n    linkedin: \"https://linkedin.com\",\n    github: \"https://github.com\",\n    discord: \"https://discord.com\",\n  },\n  copyright = \"© 2024 Smoothui. All rights reserved.\",\n}: FooterSimpleProps) {\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={{ opacity: 1, y: 0 }}\n          className=\"grid grid-cols-1 gap-8 md:grid-cols-2 lg:grid-cols-5\"\n          initial={{ opacity: 0, y: 20 }}\n          transition={{ duration: ANIMATION_DURATION }}\n          viewport={{ once: true }}\n          whileInView={{ opacity: 1, y: 0 }}\n        >\n          {/* Company Info */}\n          <div className=\"lg:col-span-2\">\n            <motion.div\n              animate={{ opacity: 1, y: 0 }}\n              initial={{ opacity: 0, y: 20 }}\n              transition={{\n                duration: ANIMATION_DURATION,\n                delay: DELAY_INCREMENT,\n              }}\n              viewport={{ once: true }}\n              whileInView={{ opacity: 1, y: 0 }}\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={{ scale: HOVER_SCALE }}\n                    whileTap={{ 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                )}\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={{ scale: HOVER_SCALE }}\n                    whileTap={{ 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                )}\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={{ scale: HOVER_SCALE }}\n                    whileTap={{ 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                )}\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={{ scale: HOVER_SCALE }}\n                    whileTap={{ 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                )}\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={{ opacity: 1, y: 0 }}\n                initial={{ opacity: 0, y: 20 }}\n                transition={{\n                  duration: ANIMATION_DURATION,\n                  delay: DELAY_PRODUCT,\n                }}\n                viewport={{ once: true }}\n                whileInView={{ opacity: 1, y: 0 }}\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            )}\n\n            {links.company && (\n              <motion.div\n                animate={{ opacity: 1, y: 0 }}\n                initial={{ opacity: 0, y: 20 }}\n                transition={{\n                  duration: ANIMATION_DURATION,\n                  delay: DELAY_COMPANY,\n                }}\n                viewport={{ once: true }}\n                whileInView={{ opacity: 1, y: 0 }}\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            )}\n\n            {links.support && (\n              <motion.div\n                animate={{ opacity: 1, y: 0 }}\n                initial={{ opacity: 0, y: 20 }}\n                transition={{\n                  duration: ANIMATION_DURATION,\n                  delay: DELAY_SUPPORT,\n                }}\n                viewport={{ once: true }}\n                whileInView={{ opacity: 1, y: 0 }}\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            )}\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            duration: ANIMATION_DURATION,\n            delay: DELAY_COPYRIGHT,\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 FooterSimple;\n","target":"components/smoothui/footer-1/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"footer-2","type":"registry:ui","title":"footer-2","description":"Complex footer block with newsletter and extended links","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion"],"devDependencies":[],"registryDependencies":["https://smoothui.dev/r/smooth-button.json"],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport SmoothButton from \"@repo/smoothui/components/smooth-button\";\nimport { motion } 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 BUTTON_HOVER_SCALE = 1.02;\nconst BUTTON_TAP_SCALE = 0.98;\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    title: \"Stay updated\",\n    description: \"Get the latest news and updates delivered to your inbox.\",\n    placeholder: \"Enter your email\",\n    buttonText: \"Subscribe\",\n  },\n  links = {\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    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    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    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  },\n  social = {\n    twitter: \"https://twitter.com\",\n    linkedin: \"https://linkedin.com\",\n    github: \"https://github.com\",\n    discord: \"https://discord.com\",\n    youtube: \"https://youtube.com\",\n  },\n  copyright = \"© 2024 Smoothui. All rights reserved.\",\n}: FooterComplexProps) {\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={{ opacity: 1, y: 0 }}\n          className=\"grid grid-cols-1 gap-12 lg:grid-cols-12\"\n          initial={{ opacity: 0, y: 20 }}\n          transition={{ duration: ANIMATION_DURATION }}\n          viewport={{ once: true }}\n          whileInView={{ opacity: 1, y: 0 }}\n        >\n          {/* Company Info & Newsletter */}\n          <div className=\"lg:col-span-5\">\n            <motion.div\n              animate={{ opacity: 1, y: 0 }}\n              initial={{ opacity: 0, y: 20 }}\n              transition={{\n                duration: ANIMATION_DURATION,\n                delay: DELAY_INCREMENT,\n              }}\n              viewport={{ once: true }}\n              whileInView={{ opacity: 1, y: 0 }}\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={{ scale: HOVER_SCALE }}\n                    whileTap={{ 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                )}\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={{ scale: HOVER_SCALE }}\n                    whileTap={{ 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                )}\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={{ scale: HOVER_SCALE }}\n                    whileTap={{ 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                )}\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={{ scale: HOVER_SCALE }}\n                    whileTap={{ 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                )}\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={{ scale: HOVER_SCALE }}\n                    whileTap={{ 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.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                )}\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={{ opacity: 1, y: 0 }}\n                initial={{ opacity: 0, y: 20 }}\n                transition={{\n                  duration: ANIMATION_DURATION,\n                  delay: DELAY_PRODUCT,\n                }}\n                viewport={{ once: true }}\n                whileInView={{ opacity: 1, y: 0 }}\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            )}\n\n            {links.company && (\n              <motion.div\n                animate={{ opacity: 1, y: 0 }}\n                initial={{ opacity: 0, y: 20 }}\n                transition={{\n                  duration: ANIMATION_DURATION,\n                  delay: DELAY_COMPANY,\n                }}\n                viewport={{ once: true }}\n                whileInView={{ opacity: 1, y: 0 }}\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            )}\n\n            {links.support && (\n              <motion.div\n                animate={{ opacity: 1, y: 0 }}\n                initial={{ opacity: 0, y: 20 }}\n                transition={{\n                  duration: ANIMATION_DURATION,\n                  delay: DELAY_SUPPORT,\n                }}\n                viewport={{ once: true }}\n                whileInView={{ opacity: 1, y: 0 }}\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            )}\n\n            {links.legal && (\n              <motion.div\n                animate={{ opacity: 1, y: 0 }}\n                initial={{ opacity: 0, y: 20 }}\n                transition={{\n                  duration: ANIMATION_DURATION,\n                  delay: DELAY_LEGAL,\n                }}\n                viewport={{ once: true }}\n                whileInView={{ opacity: 1, y: 0 }}\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            )}\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            duration: ANIMATION_DURATION,\n            delay: DELAY_COPYRIGHT,\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","target":"components/smoothui/footer-2/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"footer-3","type":"registry:ui","title":"footer-3","description":"Mega footer with newsletter and social links","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["lucide-react","motion"],"devDependencies":[],"registryDependencies":["https://smoothui.dev/r/smooth-button.json"],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport SmoothButton from \"@repo/smoothui/components/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    title: \"Product\",\n    links: [\n      { label: \"Features\", href: \"#features\" },\n      { label: \"Pricing\", href: \"#pricing\" },\n      { label: \"Changelog\", href: \"#changelog\" },\n      { label: \"Documentation\", href: \"#docs\" },\n    ],\n  },\n  {\n    title: \"Company\",\n    links: [\n      { label: \"About\", href: \"#about\" },\n      { label: \"Blog\", href: \"#blog\" },\n      { label: \"Careers\", href: \"#careers\" },\n      { label: \"Contact\", href: \"#contact\" },\n    ],\n  },\n  {\n    title: \"Resources\",\n    links: [\n      { label: \"Community\", href: \"#community\" },\n      { label: \"GitHub\", href: \"#github\" },\n      { label: \"Discord\", href: \"#discord\" },\n      { label: \"Help Center\", href: \"#help\" },\n    ],\n  },\n  {\n    title: \"Legal\",\n    links: [\n      { label: \"Privacy Policy\", href: \"#privacy\" },\n      { label: \"Terms of Service\", href: \"#terms\" },\n      { label: \"Cookie Policy\", href: \"#cookies\" },\n    ],\n  },\n];\n\nconst defaultSocialLinks = [\n  {\n    icon: <Twitter className=\"h-5 w-5\" />,\n    href: \"https://twitter.com\",\n    label: \"Twitter\",\n  },\n  {\n    icon: <Github className=\"h-5 w-5\" />,\n    href: \"https://github.com\",\n    label: \"GitHub\",\n  },\n  {\n    icon: <Linkedin className=\"h-5 w-5\" />,\n    href: \"https://linkedin.com\",\n    label: \"LinkedIn\",\n  },\n  {\n    icon: <Youtube className=\"h-5 w-5\" />,\n    href: \"https://youtube.com\",\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    title: \"Subscribe to our newsletter\",\n    description: \"Get the latest updates and news delivered to your inbox.\",\n    placeholder: \"Enter your email\",\n    buttonText: \"Subscribe\",\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        initial: { opacity: 1 },\n        animate: { opacity: 1 },\n        transition: { duration: 0 },\n      };\n    }\n\n    return {\n      initial: { opacity: 0, y: 20 },\n      whileInView: { opacity: 1, y: 0 },\n      viewport: { once: true },\n      transition: {\n        type: \"spring\" as const,\n        duration: ANIMATION_DURATION,\n        bounce: 0.1,\n        delay,\n      },\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","target":"components/smoothui/footer-3/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"footer-4","type":"registry:ui","title":"footer-4","description":"Minimal single-row footer","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["lucide-react","motion"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","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  { label: \"Privacy\", href: \"#privacy\" },\n  { label: \"Terms\", href: \"#terms\" },\n  { label: \"Contact\", href: \"#contact\" },\n];\n\nconst defaultSocialLinks = [\n  {\n    icon: <Twitter className=\"h-5 w-5\" />,\n    href: \"https://twitter.com\",\n    label: \"Twitter\",\n  },\n  {\n    icon: <Github className=\"h-5 w-5\" />,\n    href: \"https://github.com\",\n    label: \"GitHub\",\n  },\n  {\n    icon: <Linkedin className=\"h-5 w-5\" />,\n    href: \"https://linkedin.com\",\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        initial: { opacity: 1 },\n        animate: { opacity: 1 },\n        transition: { duration: 0 },\n      };\n    }\n\n    return {\n      initial: { opacity: 0, y: 10 },\n      whileInView: { opacity: 1, y: 0 },\n      viewport: { once: true },\n      transition: {\n        type: \"spring\" as const,\n        duration: ANIMATION_DURATION,\n        bounce: 0.1,\n      },\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","target":"components/smoothui/footer-4/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"header-1","type":"registry:ui","title":"header-1","description":"Modern header block with navigation and mobile menu","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["lucide-react","motion"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport { ExternalLink } from \"lucide-react\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { AnimatedGroup, AnimatedText, Button, HeroHeader } from \"../../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={{ width: \"100%\", height: \"100%\" }}\n    >\n      <div\n        className={styles.mainGrid}\n        style={\n          {\n            gridTemplateColumns: `repeat(${grid.columns}, 1fr)`,\n            gridTemplateRows: `repeat(${grid.rows}, 1fr)`,\n            \"--grid-cell-size\": `${CELL_SIZE}px`,\n            width: \"100%\",\n            height: \"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","target":"components/smoothui/header-1/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"header-2","type":"registry:ui","title":"header-2","description":"Premium header block with gradient logo and smooth animations","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["lucide-react","motion"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport { getImageKitUrl } from \"@smoothui/data\";\nimport { motion } from \"motion/react\";\nimport { AnimatedGroup, AnimatedText, Button, HeroHeader } from \"../../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    width: 1200,\n    quality: 85,\n    format: \"auto\",\n  }),\n  imageAlt = \"SmoothUI desktop application interface\",\n}: HeroProductProps) {\n  return (\n    <div className=\"relative\">\n      <HeroHeader />\n      <main>\n        <motion.section\n          animate={{ opacity: 1, scale: 1 }}\n          className=\"relative overflow-hidden\"\n          initial={{ opacity: 0, scale: 1.02 }}\n          transition={{\n            duration: 0.8,\n            ease: [0.25, 0.46, 0.45, 0.94], // ease-out-quad\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={{ opacity: 1, y: 0, scale: 1 }}\n                  initial={{ opacity: 0, y: 20, scale: 0.9 }}\n                  transition={{\n                    duration: 0.6,\n                    delay: 0.1,\n                    ease: [0.25, 0.46, 0.45, 0.94],\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={{ opacity: 1, y: 0 }}\n                  className=\"flex items-center justify-center gap-3\"\n                  initial={{ opacity: 0, y: 20 }}\n                  transition={{\n                    duration: 0.6,\n                    delay: 0.4,\n                    ease: [0.25, 0.46, 0.45, 0.94],\n                  }}\n                >\n                  <motion.div\n                    transition={{\n                      type: \"spring\" as const,\n                      stiffness: 400,\n                      damping: 10,\n                    }}\n                    whileHover={{ scale: 1.05 }}\n                    whileTap={{ 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                      type: \"spring\" as const,\n                      stiffness: 400,\n                      damping: 10,\n                    }}\n                    whileHover={{ scale: 1.05 }}\n                    whileTap={{ 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, y: 0, scale: 1 }}\n              className=\"relative\"\n              initial={{ opacity: 0, y: 40, scale: 0.95 }}\n              transition={{\n                duration: 0.8,\n                delay: 0.6,\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                    type: \"spring\" as const,\n                    stiffness: 300,\n                    damping: 20,\n                  }}\n                  whileHover={{\n                    scale: 1.02,\n                    rotateY: 2,\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                      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                    <motion.div\n                      animate={{ scale: 1 }}\n                      initial={{ scale: 1.05 }}\n                      transition={{\n                        duration: 1.2,\n                        delay: 0.8,\n                        ease: [0.25, 0.46, 0.45, 0.94],\n                      }}\n                    >\n                      <img\n                        alt={imageAlt}\n                        className=\"h-auto w-full\"\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","target":"components/smoothui/header-2/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"header-3","type":"registry:ui","title":"header-3","description":"Premium header block with gradient logo and smooth animations","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["lucide-react","motion"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport { Avatar, AvatarImage } from \"@repo/shadcn-ui/components/ui/avatar\";\nimport { getAllPeople, getAvatarUrl, getImageKitUrl } from \"@smoothui/data\";\nimport { ArrowDownRight, Star } from \"lucide-react\";\nimport { motion } from \"motion/react\";\nimport { AnimatedGroup, AnimatedText, Button, HeroHeader } from \"../../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    count: 200,\n    rating: 5.0,\n    avatars: getAllPeople()\n      .slice(0, 5)\n      .map((person) => ({\n        src: getAvatarUrl(person.avatar, 90),\n        alt: `${person.name} avatar`,\n      })),\n  },\n}: HeroShowcaseProps) {\n  return (\n    <>\n      <HeroHeader />\n      <main>\n        <motion.section\n          animate={{ opacity: 1, scale: 1, filter: \"blur(0px)\" }}\n          className=\"relative overflow-hidden bg-gradient-to-b from-background to-muted\"\n          initial={{ opacity: 0, scale: 1.04, filter: \"blur(12px)\" }}\n          transition={{ type: \"spring\" as const, bounce: 0.32, duration: 0.9 }}\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                        type: \"spring\" as const,\n                        stiffness: 300,\n                        damping: 20,\n                      }}\n                      whileHover={{ 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                )}\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                )}\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                height={1842}\n                src={getImageKitUrl(\"/images/hero-example_xertaz.png\", {\n                  width: 1200,\n                  quality: 85,\n                  format: \"auto\",\n                })}\n                width={2880}\n              />\n            </div>\n          </div>\n        </motion.section>\n      </main>\n    </>\n  );\n}\nexport default HeroShowcase;\n","target":"components/smoothui/header-3/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"header-4","type":"registry:ui","title":"header-4","description":"Interactive 3D grid hero header inspired by rauno.me 2024","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["lucide-react","motion"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","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 \"../../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","target":"components/smoothui/header-4/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"header-5","type":"registry:ui","title":"header-5","description":"Spotlight hero block with dark theme and gradient text","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion"],"devDependencies":[],"registryDependencies":["https://smoothui.dev/r/smooth-button.json"],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport SmoothButton from \"@repo/smoothui/components/smooth-button\";\nimport { motion, useReducedMotion } from \"motion/react\";\n\nconst SPRING = {\n  type: \"spring\" as const,\n  duration: 0.25,\n  bounce: 0.1,\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                left: `${(i * 37 + 13) % 100}%`,\n                top: `${(i * 53 + 7) % 100}%`,\n                animation: `pulse ${2 + (i % 3)}s ease-in-out infinite ${(i % 5) * 0.5}s`,\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","target":"components/smoothui/header-5/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"header-6","type":"registry:ui","title":"header-6","description":"Minimal hero block with clean typography","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport { cn } from \"@repo/shadcn-ui/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","target":"components/smoothui/header-6/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"logo-cloud-1","type":"registry:ui","title":"logo-cloud-1","description":"Simple logo cloud block with grid layout","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":[],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","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 \"../../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","target":"components/smoothui/logo-cloud-1/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"logo-cloud-2","type":"registry:ui","title":"logo-cloud-2","description":"Animated logo cloud block with infinite scroll","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport { motion } 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 \"../../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    { name: \"Canpoy\", logo: Canpoy, url: \"https://canpoy.com\" },\n    { name: \"Canva\", logo: Canva, url: \"https://canva.com\" },\n    { name: \"Casetext\", logo: Casetext, url: \"https://casetext.com\" },\n    { name: \"Strava\", logo: Strava, url: \"https://strava.com\" },\n    { name: \"Descript\", logo: Descript, url: \"https://descript.com\" },\n    { name: \"Duolingo\", logo: Duolingo, url: \"https://duolingo.com\" },\n    { name: \"Faire\", logo: Faire, url: \"https://faire.com\" },\n    { name: \"Clearbit\", logo: Clearbit, url: \"https://clearbit.com\" },\n  ],\n}: LogoCloudAnimatedProps) {\n  return (\n    <section className=\"overflow-hidden py-20\">\n      <div className=\"mx-auto max-w-7xl px-6\">\n        <motion.div\n          animate={{ opacity: 1, y: 0 }}\n          className=\"mb-16 text-center\"\n          initial={{ opacity: 0, y: 20 }}\n          transition={{ 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              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              x: {\n                repeat: Number.POSITIVE_INFINITY,\n                repeatType: \"loop\",\n                duration: ANIMATION_DURATION,\n                ease: \"linear\",\n              },\n            }}\n          >\n            {/* First set */}\n            {logos.map((logo, index) => (\n              <motion.a\n                animate={{ opacity: 1, scale: 1 }}\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={{ opacity: 0, scale: 0.8 }}\n                key={`first-${logo.name}`}\n                rel=\"noopener noreferrer\"\n                target=\"_blank\"\n                transition={{\n                  duration: 0.4,\n                  delay: index * STAGGER_DELAY,\n                }}\n              >\n                <motion.div\n                  className=\"mb-2 text-4xl *:fill-foreground\"\n                  transition={{\n                    type: \"spring\" as const,\n                    stiffness: SPRING_STIFFNESS,\n                  }}\n                  whileHover={{ scale: HOVER_SCALE, rotate: HOVER_ROTATE }}\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={{ opacity: 1, scale: 1 }}\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={{ opacity: 0, scale: 0.8 }}\n                key={`second-${logo.name}`}\n                rel=\"noopener noreferrer\"\n                target=\"_blank\"\n                transition={{\n                  duration: 0.4,\n                  delay: index * STAGGER_DELAY,\n                }}\n              >\n                <motion.div\n                  className=\"mb-2 text-4xl *:fill-foreground\"\n                  transition={{\n                    type: \"spring\" as const,\n                    stiffness: SPRING_STIFFNESS,\n                  }}\n                  whileHover={{ scale: HOVER_SCALE, rotate: HOVER_ROTATE }}\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={{ opacity: 1, scale: 1 }}\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={{ opacity: 0, scale: 0.8 }}\n                key={`third-${logo.name}`}\n                rel=\"noopener noreferrer\"\n                target=\"_blank\"\n                transition={{\n                  duration: 0.4,\n                  delay: index * STAGGER_DELAY,\n                }}\n              >\n                <motion.div\n                  className=\"mb-2 text-4xl *:fill-foreground\"\n                  transition={{\n                    type: \"spring\" as const,\n                    stiffness: SPRING_STIFFNESS,\n                  }}\n                  whileHover={{ scale: HOVER_SCALE, rotate: HOVER_ROTATE }}\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","target":"components/smoothui/logo-cloud-2/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"logo-cloud-3","type":"registry:ui","title":"logo-cloud-3","description":"Infinite scrolling logo marquee","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":[],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","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 \"../../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  { name: \"Canpoy\", logo: <Canpoy /> },\n  { name: \"Canva\", logo: <Canva /> },\n  { name: \"Casetext\", logo: <Casetext /> },\n  { name: \"Strava\", logo: <Strava /> },\n  { name: \"Descript\", logo: <Descript /> },\n  { name: \"Duolingo\", logo: <Duolingo /> },\n  { name: \"Faire\", logo: <Faire /> },\n  { name: \"Clearbit\", logo: <Clearbit /> },\n];\n\nconst SPEED_MAP = {\n  slow: \"60s\",\n  normal: \"40s\",\n  fast: \"20s\",\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-duration\": animationDuration,\n                \"--marquee-direction\": animationDirection,\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","target":"components/smoothui/logo-cloud-3/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"logo-cloud-4","type":"registry:ui","title":"logo-cloud-4","description":"Interactive logo grid with hover effects","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","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 \"../../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  { name: \"Canpoy\", logo: <Canpoy /> },\n  { name: \"Canva\", logo: <Canva /> },\n  { name: \"Casetext\", logo: <Casetext /> },\n  { name: \"Strava\", logo: <Strava /> },\n  { name: \"Descript\", logo: <Descript /> },\n  { name: \"Duolingo\", logo: <Duolingo /> },\n  { name: \"Faire\", logo: <Faire /> },\n  { name: \"Clearbit\", logo: <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              type: \"spring\" as const,\n              duration: 0.25,\n              bounce: 0.05,\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, x: \"-50%\", y: 0, scale: 1 }}\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, y: 4, scale: 0.95 }\n            }\n            initial={\n              shouldReduceMotion\n                ? { opacity: 1, x: \"-50%\", y: 0 }\n                : { opacity: 0, x: \"-50%\", y: 4, scale: 0.95 }\n            }\n            transition={\n              shouldReduceMotion\n                ? { duration: 0 }\n                : {\n                    type: \"spring\" as const,\n                    duration: 0.25,\n                    bounce: 0.05,\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        )}\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","target":"components/smoothui/logo-cloud-4/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"pricing-1","type":"registry:ui","title":"pricing-1","description":"Simple pricing block with single plan","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion"],"devDependencies":[],"registryDependencies":["https://smoothui.dev/r/smooth-button.json"],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport { cn } from \"@repo/shadcn-ui/lib/utils\";\nimport SmoothButton from \"@repo/smoothui/components/smooth-button\";\nimport { motion } 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 [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={{ opacity: 1, y: 0 }}\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={{ opacity: 0, y: 40 }}\n                transition={{ duration: 0.3, ease: [0.22, 1, 0.36, 1] }}\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","target":"components/smoothui/pricing-1/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"pricing-2","type":"registry:ui","title":"pricing-2","description":"Modern pricing block with three tiers","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion"],"devDependencies":[],"registryDependencies":["https://smoothui.dev/r/smooth-button.json"],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport { cn } from \"@repo/shadcn-ui/lib/utils\";\nimport SmoothButton from \"@repo/smoothui/components/smooth-button\";\nimport { motion } 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 [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={{ opacity: 1, y: 0 }}\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={{ opacity: 0, y: 40 }}\n                  transition={{ duration: 0.3, ease: [0.22, 1, 0.36, 1] }}\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={{ opacity: 1, y: 0 }}\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={{ opacity: 0, y: 40 }}\n                  transition={{\n                    duration: 0.3,\n                    ease: [0.22, 1, 0.36, 1],\n                    delay: CARD_ANIMATION_DELAY,\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={{ opacity: 1, y: 0 }}\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={{ opacity: 0, y: 40 }}\n                  transition={{\n                    duration: 0.3,\n                    ease: [0.22, 1, 0.36, 1],\n                    delay: CARD_ANIMATION_DELAY * 2,\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","target":"components/smoothui/pricing-2/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"pricing-3","type":"registry:ui","title":"pricing-3","description":"Creative pricing block with two plans","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion"],"devDependencies":[],"registryDependencies":["https://smoothui.dev/r/smooth-button.json"],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport { cn } from \"@repo/shadcn-ui/lib/utils\";\nimport SmoothButton from \"@repo/smoothui/components/smooth-button\";\nimport { motion } 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 [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={{ opacity: 1, y: 0 }}\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={{ opacity: 0, y: 40 }}\n                  transition={{ duration: 0.3, ease: [0.22, 1, 0.36, 1] }}\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={{ opacity: 1, y: 0 }}\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={{ opacity: 0, y: 40 }}\n                  transition={{\n                    duration: 0.3,\n                    ease: [0.22, 1, 0.36, 1],\n                    delay: CARD_ANIMATION_DELAY,\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","target":"components/smoothui/pricing-3/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"shared","type":"registry:ui","title":"shared","description":"Shared components for SmoothUI blocks","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["lucide-react","motion","class-variance-authority","@radix-ui/react-slot"],"devDependencies":[],"registryDependencies":["https://smoothui.dev/r/smooth-button.json"],"files":[{"type":"registry:ui","path":"animated-group.tsx","content":"\"use client\";\n\nimport { motion, 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  fade: {},\n  slide: {\n    hidden: { y: 20 },\n    visible: { y: 0 },\n  },\n  scale: {\n    hidden: { scale: 0.8 },\n    visible: { scale: 1 },\n  },\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  zoom: {\n    hidden: { scale: 0.5 },\n    visible: {\n      scale: 1,\n      transition: { type: \"spring\" as const, stiffness: 300, damping: 20 },\n    },\n  },\n  flip: {\n    hidden: { rotateX: -90 },\n    visible: {\n      rotateX: 0,\n      transition: { type: \"spring\" as const, stiffness: 300, damping: 20 },\n    },\n  },\n  bounce: {\n    hidden: { y: -50 },\n    visible: {\n      y: 0,\n      transition: { type: \"spring\" as const, stiffness: 400, damping: 10 },\n    },\n  },\n  rotate: {\n    hidden: { rotate: -180 },\n    visible: {\n      rotate: 0,\n      transition: { type: \"spring\" as const, stiffness: 200, damping: 15 },\n    },\n  },\n  swing: {\n    hidden: { rotate: -10 },\n    visible: {\n      rotate: 0,\n      transition: { type: \"spring\" as const, stiffness: 300, damping: 8 },\n    },\n  },\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 selectedVariants = {\n    item: addDefaultVariants(preset ? presetVariants[preset] : {}),\n    container: addDefaultVariants(defaultContainerVariants),\n  };\n  const containerVariants = variants?.container || selectedVariants.container;\n  const itemVariants = 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","target":"components/smoothui/shared/animated-group.tsx"},{"type":"registry:ui","path":"animated-text.tsx","content":"\"use client\";\n\nimport { motion } 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  return (\n    <motion.div\n      animate={{ opacity: 1, filter: \"blur(0px)\", y: 0 }}\n      initial={{ opacity: 0, filter: \"blur(12px)\", y: 12 }}\n      transition={{\n        type: \"spring\" as const,\n        bounce: 0.3,\n        duration: 1.5,\n        delay,\n      }}\n    >\n      <Tag className={className}>{children}</Tag>\n    </motion.div>\n  );\n}\n","target":"components/smoothui/shared/animated-text.tsx"},{"type":"registry:ui","path":"hero-header.tsx","content":"\"use client\";\n\nimport { cn } from \"@repo/shadcn-ui/lib/utils\";\nimport SmoothButton from \"@repo/smoothui/components/smooth-button\";\nimport { Menu, X } from \"lucide-react\";\nimport { AnimatePresence, motion } from \"motion/react\";\nimport { useState } from \"react\";\n\nconst menuItems = [\n  { id: \"features\", name: \"Features\", href: \"#link\" },\n  { id: \"pricing\", name: \"Pricing\", href: \"#link\" },\n  { id: \"about\", name: \"About\", href: \"#link\" },\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 [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:filter\", \"dark:invert\")}\n                    height={28}\n                    src=\"/logo-smoothui.svg\"\n                    width={140}\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={{ opacity: 1, rotate: 0, scale: SCALE_MAX }}\n                        exit={{\n                          opacity: 0,\n                          rotate: -ROTATION_ANGLE,\n                          scale: SCALE_MIN,\n                        }}\n                        initial={{\n                          opacity: 0,\n                          rotate: ROTATION_ANGLE,\n                          scale: SCALE_MIN,\n                        }}\n                        key=\"close\"\n                        transition={{\n                          duration: ANIMATION_DURATION,\n                          ease: EASE_OUT_QUART,\n                        }}\n                      >\n                        <X className=\"m-auto size-6\" />\n                      </motion.div>\n                    ) : (\n                      <motion.div\n                        animate={{ opacity: 1, rotate: 0, scale: SCALE_MAX }}\n                        exit={{\n                          opacity: 0,\n                          rotate: ROTATION_ANGLE,\n                          scale: SCALE_MIN,\n                        }}\n                        initial={{\n                          opacity: 0,\n                          rotate: -ROTATION_ANGLE,\n                          scale: SCALE_MIN,\n                        }}\n                        key=\"menu\"\n                        transition={{\n                          duration: ANIMATION_DURATION,\n                          ease: EASE_OUT_QUART,\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={{ opacity: 1, y: 0, scale: SCALE_MAX }}\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={{ opacity: 0, y: TRANSLATE_Y_OFFSET, scale: 0.95 }}\n                    initial={{ opacity: 0, y: TRANSLATE_Y_OFFSET, scale: 0.95 }}\n                    transition={{\n                      duration: ANIMATION_DURATION,\n                      ease: EASE_OUT_QUART,\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={{ opacity: 1, x: 0 }}\n                            initial={{ opacity: 0, x: TRANSLATE_X_OFFSET }}\n                            key={item.id}\n                            transition={{\n                              delay: index * STAGGER_DELAY,\n                              duration: ANIMATION_DURATION,\n                              ease: EASE_OUT_QUART,\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={{ opacity: 1, y: 0 }}\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={{ opacity: 0, y: 10 }}\n                      transition={{\n                        delay: menuItems.length * STAGGER_DELAY + STAGGER_DELAY,\n                        duration: ANIMATION_DURATION,\n                        ease: EASE_OUT_QUART,\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                )}\n              </AnimatePresence>\n            </div>\n          </div>\n        </nav>\n      </header>\n    </div>\n  );\n};\n","target":"components/smoothui/shared/hero-header.tsx"},{"type":"registry:ui","path":"logos.tsx","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=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\n          />\n          <path\n            d=\"M48.8892 61.5464H45.5947V75.9277H48.8892V61.5464Z\"\n            fill=\"var(--color-gray12)\"\n          />\n          <path\n            d=\"M48.9958 56.2085H45.457V59.3421H48.9958V56.2085Z\"\n            fill=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\n          />\n          <path\n            d=\"M80.0465 56.1609H76.5078V59.2945H80.0465V56.1609Z\"\n            fill=\"var(--color-gray12)\"\n          />\n          <path\n            d=\"M76.6445 61.5464V75.9277H77.9552H79.939V75.1234V61.5464H76.6445Z\"\n            fill=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\n          />\n          <path\n            d=\"M115.416 57.5488H112.121V61.8501V62.0586L115.416 60.6705V57.5488Z\"\n            fill=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\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=\"var(--color-gray12)\"\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","target":"components/smoothui/shared/logos.tsx"},{"type":"registry:ui","path":"smoothbutton.tsx","content":"import { Slot } from \"@radix-ui/react-slot\";\nimport { cn } from \"@repo/shadcn-ui/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    variants: {\n      variant: {\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        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        ghost: \"hover:bg-background hover:text-foreground hover:shadow-custom\",\n        link: \"text-primary underline-offset-4 hover:underline\",\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      },\n      size: {\n        default: \"h-10 px-4 py-2\",\n        sm: \"h-9 rounded-md px-4 py-2\",\n        lg: \"h-11 rounded-md px-8\",\n        icon: \"h-10 w-10\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\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({ variant, size, className }))}\n      ref={ref}\n      {...props}\n    />\n  );\n}\n\nexport { Button, buttonVariants };\n","target":"components/smoothui/shared/smoothbutton.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"stats-1","type":"registry:ui","title":"stats-1","description":"Grid stats section block with hover effects","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport { motion, useInView } 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      value: \"10M+\",\n      label: \"Active Users\",\n      description: \"Growing every day\",\n    },\n    {\n      value: \"99.9%\",\n      label: \"Uptime\",\n      description: \"Reliable service\",\n    },\n    {\n      value: \"150+\",\n      label: \"Countries\",\n      description: \"Worldwide reach\",\n    },\n    {\n      value: \"24/7\",\n      label: \"Support\",\n      description: \"Always here to help\",\n    },\n  ],\n}: StatsGridProps) {\n  const ref = useRef(null);\n  const isInView = useInView(ref, { once: true });\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={{ opacity: 0, y: 20 }}\n          transition={{ duration: 0.6 }}\n          viewport={{ once: true }}\n          whileInView={{ opacity: 1, y: 0 }}\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={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 30 }}\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={{ opacity: 0, y: 30 }}\n              key={stat.label}\n              transition={{ duration: 0.6, delay: index * STAGGER_DELAY }}\n            >\n              <motion.div\n                animate={isInView ? { scale: 1 } : { scale: 0.5 }}\n                className=\"mb-2 font-bold text-4xl text-brand lg:text-5xl\"\n                initial={{ scale: 0.5 }}\n                transition={{\n                  duration: 0.8,\n                  delay: index * STAGGER_DELAY + VALUE_DELAY_OFFSET,\n                  type: \"spring\" as const,\n                  stiffness: 200,\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              )}\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","target":"components/smoothui/stats-1/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"stats-2","type":"registry:ui","title":"stats-2","description":"Cards stats section block with icons and trends","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["lucide-react","motion"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport { DollarSign, Smartphone, Star, Users } from \"lucide-react\";\nimport { motion, useInView } 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  Users,\n  Star,\n  Smartphone,\n};\n\nexport function StatsCards({\n  title = \"Key Metrics\",\n  description = \"Track your success with these important numbers\",\n  stats = [\n    {\n      value: \"2.5M\",\n      label: \"Revenue\",\n      description: \"Annual recurring revenue\",\n      icon: \"DollarSign\",\n      trend: { value: \"+12%\", direction: \"up\" },\n    },\n    {\n      value: \"45K\",\n      label: \"Customers\",\n      description: \"Happy customers worldwide\",\n      icon: \"Users\",\n      trend: { value: \"+8%\", direction: \"up\" },\n    },\n    {\n      value: \"98%\",\n      label: \"Satisfaction\",\n      description: \"Customer satisfaction rate\",\n      icon: \"Star\",\n      trend: { value: \"+2%\", direction: \"up\" },\n    },\n    {\n      value: \"1.2M\",\n      label: \"Downloads\",\n      description: \"Total app downloads\",\n      icon: \"Smartphone\",\n      trend: { value: \"+15%\", direction: \"up\" },\n    },\n  ],\n}: StatsCardsProps) {\n  const ref = useRef(null);\n  const isInView = useInView(ref, { once: true });\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={{ opacity: 0, y: 20 }}\n          transition={{ duration: 0.6 }}\n          viewport={{ once: true }}\n          whileInView={{ opacity: 1, y: 0 }}\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                isInView\n                  ? { opacity: 1, y: 0, scale: 1 }\n                  : { opacity: 0, y: 30, scale: 0.9 }\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={{ opacity: 0, y: 30, scale: 0.9 }}\n              key={stat.label}\n              transition={{\n                duration: 0.6,\n                delay: index * STAGGER_DELAY,\n                type: \"spring\" as const,\n                stiffness: 100,\n              }}\n            >\n              {/* Icon */}\n              <motion.div\n                animate={\n                  isInView\n                    ? { rotate: 0, scale: 1 }\n                    : { rotate: -10, scale: 0.8 }\n                }\n                className=\"mb-4 text-3xl\"\n                initial={{ rotate: -10, scale: 0.8 }}\n                transition={{\n                  duration: 0.6,\n                  delay: index * STAGGER_DELAY + ICON_STAGGER_OFFSET,\n                  type: \"spring\" as const,\n                  stiffness: 200,\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={isInView ? { scale: 1 } : { scale: 0.5 }}\n                className=\"mb-1 font-bold text-2xl text-foreground lg:text-3xl\"\n                initial={{ scale: 0.5 }}\n                transition={{\n                  duration: 0.8,\n                  delay: index * STAGGER_DELAY + VALUE_DELAY_OFFSET,\n                  type: \"spring\" as const,\n                  stiffness: 200,\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              )}\n\n              {/* Trend */}\n              {stat.trend && (\n                <motion.div\n                  animate={\n                    isInView ? { opacity: 1, x: 0 } : { 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={{ opacity: 0, x: -10 }}\n                  transition={{\n                    duration: 0.4,\n                    delay: index * STAGGER_DELAY + TREND_STAGGER_OFFSET,\n                  }}\n                >\n                  <span className=\"mr-1\">\n                    {stat.trend.direction === \"up\" ? \"↗\" : \"↘\"}\n                  </span>\n                  {stat.trend.value}\n                </motion.div>\n              )}\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","target":"components/smoothui/stats-2/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"team-1","type":"registry:ui","title":"team-1","description":"Grid team section block with hover effects","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["@smoothui/data","motion"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport { getAllPeople, getAvatarUrl, type Person } from \"@smoothui/data\";\nimport { motion, useInView } 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 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={{ opacity: 0, y: 20 }}\n          transition={{ duration: 0.6 }}\n          viewport={{ once: true }}\n          whileInView={{ opacity: 1, y: 0 }}\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={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 30 }}\n              initial={{ opacity: 0, y: 30 }}\n              key={member.name}\n              transition={{ duration: 0.6, delay: index * STAGGER_DELAY }}\n            >\n              <motion.div\n                className=\"group\"\n                transition={{\n                  type: \"spring\" as const,\n                  stiffness: 300,\n                  damping: 20,\n                }}\n                whileHover={{ scale: 1.02 }}\n              >\n                {/* Avatar */}\n                <motion.div\n                  className=\"relative overflow-hidden rounded-2xl\"\n                  transition={{\n                    type: \"spring\" as const,\n                    stiffness: 300,\n                    damping: 20,\n                  }}\n                  whileHover={{ 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                    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={{ duration: 0.3 }}\n                    whileHover={{ 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                )}\n                {/* Bio */}\n                {member.bio && (\n                  <p className=\"mt-2 text-foreground/70 text-sm\">\n                    {member.bio}\n                  </p>\n                )}\n              </motion.div>\n            </motion.li>\n          ))}\n        </motion.ul>\n      </div>\n    </section>\n  );\n}\n\nexport default TeamGrid;\n","target":"components/smoothui/team-1/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"team-2","type":"registry:ui","title":"team-2","description":"Carousel team section block with auto-play","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["@smoothui/data","motion"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport { getAllPeople, getAvatarUrl, type Person } from \"@smoothui/data\";\nimport { motion } 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 [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={{ opacity: 0, y: 20 }}\n          transition={{ duration: 0.6 }}\n          viewport={{ once: true }}\n          whileInView={{ opacity: 1, y: 0 }}\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 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={{ scale: 1.05 }}\n              whileTap={{ 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 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={{ scale: 1.05 }}\n              whileTap={{ 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={{\n                  x: `-${currentIndex * (CARD_WIDTH + CARD_GAP)}px`,\n                }}\n                className=\"-ml-4 flex max-w-[min(calc(100vw-4rem),24rem)] select-none\"\n                transition={{\n                  type: \"spring\" as const,\n                  stiffness: 300,\n                  damping: 30,\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={{ opacity: 0, y: 20 }}\n                      transition={{\n                        duration: 0.5,\n                        delay: index * STAGGER_DELAY,\n                      }}\n                      viewport={{ once: true }}\n                      whileInView={{ opacity: 1, y: 0 }}\n                    >\n                      <img\n                        alt={member.name}\n                        className=\"mx-auto size-20 rounded-full border border-border\"\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","target":"components/smoothui/team-2/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"testimonials-1","type":"registry:ui","title":"testimonials-1","description":"Simple testimonials block with auto-rotating cards","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["@smoothui/data","motion"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport { getAvatarUrl, getTestimonials } from \"@smoothui/data\";\nimport { AnimatePresence, motion } 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  quote: testimonial.content || \"\",\n  avatar: testimonial.avatar,\n  name: testimonial.name,\n  role: testimonial.role,\n}));\n\nexport function TestimonialsSimple() {\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={{ opacity: 1, y: 0 }}\n              className=\"mb-8 text-center font-semibold text-2xl text-foreground leading-tight md:text-4xl\"\n              exit={{ opacity: 0, y: -30 }}\n              initial={{ opacity: 0, y: 30 }}\n              key={index}\n              transition={{ type: \"spring\" as const, duration: 0.5 }}\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={{ opacity: 1, filter: \"blur(0px)\" }}\n              className=\"flex items-center gap-4\"\n              exit={{ opacity: 0, filter: \"blur(8px)\" }}\n              initial={{ opacity: 0, filter: \"blur(8px)\" }}\n              key={index}\n              transition={{ type: \"spring\" as const, duration: 0.5 }}\n            >\n              <img\n                alt={`${testimonials[index].name} avatar`}\n                className=\"h-12 w-12 rounded-full border bg-foreground/10 object-cover\"\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                  width: isActive ? BAR_WIDTH : CIRCLE_SIZE,\n                  height: CIRCLE_SIZE,\n                  borderRadius: isActive\n                    ? BORDER_RADIUS_ACTIVE\n                    : BORDER_RADIUS_INACTIVE,\n                }}\n                className=\"relative block overflow-hidden bg-foreground/10\"\n                initial={false}\n                key={`testimonial-${testimonial.name}-${i}`}\n                layout\n                style={{\n                  minWidth: CIRCLE_SIZE,\n                  maxWidth: BAR_WIDTH,\n                  border: \"none\",\n                }}\n                transition={{\n                  type: \"spring\" as const,\n                  stiffness: 300,\n                  damping: 30,\n                  duration: 0.4,\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={{ width: 0 }}\n                    key={index}\n                    transition={{\n                      duration: DURATION / MILLISECONDS_TO_SECONDS,\n                      ease: \"linear\",\n                    }}\n                  />\n                )}\n              </motion.span>\n            );\n          })}\n        </div>\n      </div>\n    </section>\n  );\n}\n\nexport default TestimonialsSimple;\n","target":"components/smoothui/testimonials-1/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"testimonials-2","type":"registry:ui","title":"testimonials-2","description":"Grid testimonials block with navigation arrows","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["@smoothui/data","lucide-react","motion"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport {\n  Avatar,\n  AvatarFallback,\n  AvatarImage,\n} from \"@repo/shadcn-ui/components/ui/avatar\";\nimport { getAvatarUrl, getTestimonials } from \"@smoothui/data\";\nimport { ChevronLeft, ChevronRight } from \"lucide-react\";\nimport { AnimatePresence, motion } from \"motion/react\";\nimport { useCallback, useEffect, useState } from \"react\";\n\nconst testimonials = getTestimonials(7);\n\nexport function TestimonialsGrid() {\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={{ opacity: 1, y: 0 }}\n            className=\"mb-12\"\n            initial={{ opacity: 0, y: 20 }}\n            transition={{ duration: 0.6, ease: [0.22, 1, 0.36, 1] }}\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={{ opacity: 1, x: 0 }}\n                    className=\"group/button flex h-8 w-8 items-center justify-center rounded-full border bg-background shadow-lg transition-all duration-200 hover:scale-110 hover:shadow-xl\"\n                    initial={{ opacity: 0, x: -20 }}\n                    onClick={handlePrev}\n                    transition={{ duration: 0.3, delay: 0.4 }}\n                    type=\"button\"\n                    whileHover={{ scale: 1.1 }}\n                    whileTap={{ 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={{ opacity: 1, x: 0 }}\n                    className=\"group/button flex h-8 w-8 items-center justify-center rounded-full border bg-background shadow-lg transition-all duration-200 hover:scale-110 hover:shadow-xl\"\n                    initial={{ opacity: 0, x: 20 }}\n                    onClick={handleNext}\n                    transition={{ duration: 0.3, delay: 0.4 }}\n                    type=\"button\"\n                    whileHover={{ scale: 1.1 }}\n                    whileTap={{ 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                          opacity: isActive(index) ? 1 : 0,\n                          scale: isActive(index) ? 1 : 0.95,\n                          y: isActive(index) ? 0 : 30,\n                        }}\n                        className={`absolute inset-0 min-h-fit ${isActive(index) ? \"z-10\" : \"z-0\"}`}\n                        exit={{\n                          opacity: 0,\n                          scale: 0.9,\n                          y: -30,\n                        }}\n                        initial={{\n                          opacity: 0,\n                          scale: 0.9,\n                          y: 30,\n                        }}\n                        key={testimonial.name}\n                        transition={{\n                          duration: 0.4,\n                          ease: \"easeInOut\",\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={{ opacity: 1, y: 0 }}\n                            className=\"mb-6 text-foreground text-lg\"\n                            initial={{ opacity: 0, y: 10 }}\n                            key={active}\n                            transition={{ duration: 0.3 }}\n                          >\n                            {(testimonial.content || \"\")\n                              .split(\" \")\n                              .map((word, wordIndex) => (\n                                <motion.span\n                                  animate={{\n                                    filter: \"blur(0px)\",\n                                    opacity: 1,\n                                    y: 0,\n                                  }}\n                                  className=\"inline-block\"\n                                  initial={{\n                                    filter: \"blur(4px)\",\n                                    opacity: 0,\n                                    y: 5,\n                                  }}\n                                  key={`${testimonial.name}-word-${wordIndex}`}\n                                  transition={{\n                                    duration: 0.2,\n                                    ease: \"easeInOut\",\n                                    delay: wordIndex * 0.02,\n                                  }}\n                                >\n                                  {word}&nbsp;\n                                </motion.span>\n                              ))}\n                          </motion.p>\n                          <motion.div\n                            animate={{ opacity: 1, y: 0 }}\n                            className=\"flex items-center gap-3\"\n                            initial={{ opacity: 0, y: 10 }}\n                            transition={{ duration: 0.3, delay: 0.2 }}\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","target":"components/smoothui/testimonials-2/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"testimonials-3","type":"registry:ui","title":"testimonials-3","description":"Star testimonials block with grid layout","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["@smoothui/data","lucide-react","motion"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport {\n  Avatar,\n  AvatarFallback,\n  AvatarImage,\n} from \"@repo/shadcn-ui/components/ui/avatar\";\nimport { cn } from \"@repo/shadcn-ui/lib/utils\";\nimport { getAvatarUrl, getTestimonials } from \"@smoothui/data\";\nimport { Star } from \"lucide-react\";\nimport { motion } from \"motion/react\";\n\nconst testimonials = getTestimonials(4);\n\nexport function TestimonialsStars() {\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={{ opacity: 1, y: 0 }}\n            className=\"mb-12\"\n            initial={{ opacity: 0, y: 20 }}\n            transition={{ duration: 0.6, ease: [0.22, 1, 0.36, 1] }}\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={{ opacity: 1 }}\n            className=\"grid 3xl:grid-cols-3 3xl:gap-12 gap-6 lg:grid-cols-2\"\n            initial={{ opacity: 0 }}\n            transition={{ duration: 0.6, ease: [0.22, 1, 0.36, 1], delay: 0.2 }}\n          >\n            {testimonials.map((testimonial, index) => (\n              <motion.div\n                animate={{ opacity: 1, y: 0 }}\n                className=\"group rounded-2xl border border-transparent px-4 py-3 duration-200 hover:border-border hover:bg-background/50\"\n                initial={{ opacity: 0, y: 30 }}\n                key={testimonial.name}\n                transition={{\n                  duration: 0.5,\n                  ease: [0.22, 1, 0.36, 1],\n                  delay: index * 0.15,\n                }}\n                whileHover={{\n                  y: -4,\n                  transition: { duration: 0.2, ease: [0.22, 1, 0.36, 1] },\n                }}\n              >\n                <motion.div\n                  animate={{ opacity: 1, scale: 1 }}\n                  className=\"flex gap-1\"\n                  initial={{ opacity: 0, scale: 0.8 }}\n                  transition={{\n                    duration: 0.4,\n                    delay: index * 0.15 + 0.2,\n                    ease: [0.22, 1, 0.36, 1],\n                  }}\n                >\n                  {Array.from({ length: 5 }).map((_, i) => (\n                    <motion.div\n                      animate={{ opacity: 1, scale: 1 }}\n                      initial={{ opacity: 0, scale: 0 }}\n                      key={`${testimonial.name}-star-${i}`}\n                      transition={{\n                        duration: 0.3,\n                        delay: index * 0.15 + 0.2 + i * 0.05,\n                        ease: [0.68, -0.55, 0.265, 1.55],\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={{ opacity: 1, y: 0 }}\n                  className=\"my-4 text-foreground\"\n                  initial={{ opacity: 0, y: 10 }}\n                  transition={{\n                    duration: 0.4,\n                    delay: index * 0.15 + 0.4,\n                    ease: [0.22, 1, 0.36, 1],\n                  }}\n                >\n                  {testimonial.content}\n                </motion.p>\n\n                <motion.div\n                  animate={{ opacity: 1, x: 0 }}\n                  className=\"flex items-center gap-2\"\n                  initial={{ opacity: 0, x: -10 }}\n                  transition={{\n                    duration: 0.3,\n                    delay: index * 0.15 + 0.5,\n                    ease: [0.22, 1, 0.36, 1],\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","target":"components/smoothui/testimonials-3/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"agent-avatar","type":"registry:ui","title":"agent-avatar","description":"Canvas-based generative pixel avatar for AI agents, unique per seed.","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":[],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport { cn } from \"@repo/shadcn-ui/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        colorIndex: Math.floor(rng() * 3),\n        phase: rng() * Math.PI * 2,\n        brightness: 0.3 + rng() * 0.7,\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={{ width: size, height: size }}\n      {...props}\n    />\n  );\n};\n\nexport default AgentAvatar;\n","target":"components/smoothui/agent-avatar/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"ai-branch","type":"registry:ui","title":"ai-branch","description":"An interactive AI branch component for displaying conversation flows.","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["lucide-react","motion"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport { cn } from \"@repo/shadcn-ui/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\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    currentBranch,\n    totalBranches: branches.length,\n    goToPrevious,\n    goToNext,\n    branches,\n    setBranches,\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              opacity: index === currentBranch ? 1 : 0,\n              y: index === currentBranch ? 0 : 10,\n              display: index === currentBranch ? \"block\" : \"none\",\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={\n        shouldReduceMotion\n          ? { duration: 0 }\n          : {\n              duration: 0.25,\n              type: \"spring\" as const,\n              stiffness: 300,\n              damping: 30,\n            }\n      }\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 rounded-full text-muted-foreground transition-colors\",\n        \"hover:bg-accent 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={\n        shouldReduceMotion\n          ? { duration: 0 }\n          : {\n              type: \"spring\" as const,\n              stiffness: 400,\n              damping: 25,\n              duration: 0.2,\n            }\n      }\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 rounded-full text-muted-foreground transition-colors\",\n        \"hover:bg-accent 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={\n        shouldReduceMotion\n          ? { duration: 0 }\n          : {\n              type: \"spring\" as const,\n              stiffness: 400,\n              damping: 25,\n              duration: 0.2,\n            }\n      }\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={{\n            duration: 0.3,\n            type: \"spring\" as const,\n            stiffness: 300,\n            damping: 30,\n          }}\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-lg bg-brand p-3 text-white\">\n                <p className=\"text-sm\">{activeBranch.userMessage}</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 rounded text-foreground/70 transition-colors\",\n                      \"hover:bg-accent hover:text-white\",\n                      \"flex items-center justify-center\"\n                    )}\n                    transition={\n                      shouldReduceMotion\n                        ? { duration: 0 }\n                        : {\n                            type: \"spring\" as const,\n                            stiffness: 400,\n                            damping: 25,\n                            duration: 0.2,\n                          }\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 rounded text-foreground/70 transition-colors\",\n                      \"hover:bg-accent hover:text-white\",\n                      \"flex items-center justify-center\"\n                    )}\n                    transition={\n                      shouldReduceMotion\n                        ? { duration: 0 }\n                        : {\n                            type: \"spring\" as const,\n                            stiffness: 400,\n                            damping: 25,\n                            duration: 0.2,\n                          }\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 rounded text-foreground/70 transition-colors\",\n                      \"hover:bg-accent hover:text-white\",\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\n                        ? { duration: 0 }\n                        : {\n                            type: \"spring\" as const,\n                            stiffness: 400,\n                            damping: 25,\n                            duration: 0.2,\n                          }\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 rounded text-foreground/70 transition-colors\",\n                      \"hover:bg-accent hover:text-white\",\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\n                        ? { duration: 0 }\n                        : {\n                            type: \"spring\" as const,\n                            stiffness: 400,\n                            damping: 25,\n                            duration: 0.2,\n                          }\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-lg border border-brand/30 bg-brand/10 p-3\">\n              <p className=\"text-gray-900 text-sm dark:text-gray-100\">\n                {activeBranch.aiResponse}\n              </p>\n            </div>\n          </div>\n        </motion.div>\n      )}\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","target":"components/smoothui/ai-branch/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"ai-input","type":"registry:ui","title":"ai-input","description":"A AiInput component for SmoothUI.","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion","class-variance-authority"],"devDependencies":[],"registryDependencies":["https://smoothui.dev/r/smooth-button.json","https://smoothui.dev/r/siri-orb.json"],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport SmoothButton from \"@repo/smoothui/components/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      showFeedback,\n      success,\n      openFeedback,\n      closeFeedback,\n    }),\n    [showFeedback, success, openFeedback, closeFeedback]\n  );\n\n  return (\n    <div\n      className=\"flex items-center justify-center\"\n      style={{\n        width: FEEDBACK_WIDTH,\n        height: FEEDBACK_HEIGHT,\n      }}\n    >\n      <motion.div\n        animate={\n          shouldReduceMotion\n            ? {}\n            : {\n                width: showFeedback ? FEEDBACK_WIDTH : \"auto\",\n                height: showFeedback ? FEEDBACK_HEIGHT : DOCK_HEIGHT,\n                borderRadius: showFeedback\n                  ? FEEDBACK_BORDER_RADIUS\n                  : DOCK_BORDER_RADIUS,\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                type: \"spring\" as const,\n                stiffness: SPRING_STIFFNESS / SPEED,\n                damping: SPRING_DAMPING,\n                mass: SPRING_MASS,\n                delay: showFeedback ? 0 : CLOSE_DELAY,\n                duration: 0.25,\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        width: FEEDBACK_WIDTH,\n        height: FEEDBACK_HEIGHT,\n        pointerEvents: showFeedback ? \"all\" : \"none\",\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                    type: \"spring\" as const,\n                    stiffness: SPRING_STIFFNESS / SPEED,\n                    damping: SPRING_DAMPING,\n                    mass: SPRING_MASS,\n                    duration: 0.25,\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        )}\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        )}\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","target":"components/smoothui/ai-input/index.tsx"},{"type":"registry:ui","path":"use-click-outside.tsx","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","target":"components/smoothui/ai-input/use-click-outside.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"animated-avatar-group","type":"registry:ui","title":"animated-avatar-group","description":"Stack of overlapping avatars with smooth expand/collapse animation","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport { cn } from \"@repo/shadcn-ui/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    : { type: \"spring\" as const, duration: 0.25, bounce: 0.1 };\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            height={size}\n            initial={\n              shouldReduceMotion ? { opacity: 1 } : { opacity: 1, scale: 1 }\n            }\n            src={avatar.src}\n            style={{\n              width: size,\n              height: 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              zIndex: visibleAvatars.length - index,\n              width: size,\n              height: size,\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                width: size,\n                height: 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            width: size,\n            height: 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","target":"components/smoothui/animated-avatar-group/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"animated-file-upload","type":"registry:ui","title":"animated-file-upload","description":"Animated drag-and-drop file upload component","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport { cn } from \"@repo/shadcn-ui/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  type: \"spring\" as const,\n  duration: 0.25,\n  bounce: 0.1,\n};\n\nconst SPRING_BOUNCY = {\n  type: \"spring\" as const,\n  duration: 0.3,\n  bounce: 0.2,\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-primary\" : \"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        return;\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)}`}\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        )}\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, x: 0, scale: 1 }\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                        x: 24,\n                        scale: 0.95,\n                        transition: { duration: 0.15 },\n                      }\n                }\n                initial={\n                  shouldReduceMotion\n                    ? { opacity: 0 }\n                    : { opacity: 0, x: -16, scale: 0.95 }\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","target":"components/smoothui/animated-file-upload/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"animated-input","type":"registry:ui","title":"animated-input","description":"A AnimatedInput component for SmoothUI.","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","content":"import { motion, useReducedMotion } from \"motion/react\";\nimport { 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;\nconst RADIX_BASE_36 = 36;\nconst RANDOM_ID_START_INDEX = 2;\nconst RANDOM_ID_LENGTH = 9;\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 inputId = `animated-input-${Math.random().toString(RADIX_BASE_36).substring(RANDOM_ID_START_INDEX, RANDOM_ID_LENGTH)}`;\n\n  const getLabelAnimation = () => {\n    if (shouldReduceMotion) {\n      return {};\n    }\n    if (isFloating) {\n      return {\n        y: -24,\n        scale: 0.85,\n        color: \"var(--color-brand)\",\n        borderColor: \"var(--color-brand)\",\n      };\n    }\n    return { y: 0, scale: 1, color: \"#6b7280\" };\n  };\n\n  const getLabelStyle = () => {\n    if (!shouldReduceMotion) {\n      return {};\n    }\n    if (isFloating) {\n      return {\n        transform: \"translateY(-24px) scale(0.85)\",\n        color: \"var(--color-brand)\",\n        borderColor: \"var(--color-brand)\",\n      };\n    }\n    return {\n      transform: \"translateY(0) scale(1)\",\n      color: \"#6b7280\",\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      )}\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-primary 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","target":"components/smoothui/animated-input/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"animated-o-t-p-input","type":"registry:ui","title":"animated-o-t-p-input","description":"A AnimatedOTPInput component for SmoothUI.","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion","lucide-react","input-otp"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport { cn } from \"@repo/shadcn-ui/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              y: 0,\n              scale: isFilled ? SCALE_FILLED : 1,\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              duration: ANIMATION_DURATION_STANDARD,\n              delay: index * STAGGER_DELAY,\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, scale: 1, rotateY: 0 }\n            }\n            className=\"font-medium\"\n            exit={\n              shouldReduceMotion\n                ? { opacity: 0, transition: { duration: 0 } }\n                : { opacity: 0, scale: 0.5, rotateY: 90 }\n            }\n            initial={\n              shouldReduceMotion\n                ? { opacity: 1, scale: 1 }\n                : { opacity: 0, scale: 0.5, rotateY: -90 }\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        )}\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              repeat: Number.POSITIVE_INFINITY,\n              ease: [0.645, 0.045, 0.355, 1],\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              duration: ANIMATION_DURATION_LONG,\n              delay: SEPARATOR_DELAY,\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","target":"components/smoothui/animated-o-t-p-input/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"animated-progress-bar","type":"registry:ui","title":"animated-progress-bar","description":"A AnimatedProgressBar component for SmoothUI.","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","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  type: \"spring\" as const,\n  damping: 10,\n  mass: 0.75,\n  stiffness: 100,\n  duration: 0.25,\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      )}\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","target":"components/smoothui/animated-progress-bar/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"animated-stepper","type":"registry:ui","title":"animated-stepper","description":"Animated stepper/wizard component with step transitions","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport { cn } from \"@repo/shadcn-ui/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  type: \"spring\" as const,\n  duration: 0.25,\n  bounce: 0.1,\n};\n\nconst SPRING_BOUNCY = {\n  type: \"spring\" as const,\n  duration: 0.3,\n  bounce: 0.2,\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                  )}\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                  )}\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","target":"components/smoothui/animated-stepper/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"animated-tabs","type":"registry:ui","title":"animated-tabs","description":"Animated tabs component with sliding indicator","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport { cn } from \"@repo/shadcn-ui/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  type: \"spring\" as const,\n  duration: 0.25,\n  bounce: 0.05,\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 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 && <span className=\"relative z-10\">{tab.icon}</span>}\n            <span className=\"relative z-10\">{tab.label}</span>\n          </button>\n        );\n      })}\n    </div>\n  );\n}\n","target":"components/smoothui/animated-tabs/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"animated-tags","type":"registry:ui","title":"animated-tags","description":"A AnimatedTags component for SmoothUI.","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion","lucide-react"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","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                        y: 0,\n                        opacity: 1,\n                        filter: \"blur(0px)\",\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                    : { y: 20, opacity: 0, filter: \"blur(4px)\" }\n                }\n                initial={\n                  shouldReduceMotion\n                    ? { opacity: 1 }\n                    : { y: 20, opacity: 0, filter: \"blur(4px)\" }\n                }\n                key={tag}\n                layout\n                onClick={() => handleDeleteTag(tag)}\n                transition={\n                  shouldReduceMotion\n                    ? { duration: 0 }\n                    : { duration: 0.25, bounce: 0, 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                      y: 0,\n                      opacity: 1,\n                      filter: \"blur(0px)\",\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                  : { y: -20, opacity: 0, filter: \"blur(4px)\" }\n              }\n              initial={\n                shouldReduceMotion\n                  ? { opacity: 1 }\n                  : { y: -20, opacity: 0, filter: \"blur(4px)\" }\n              }\n              key={tag}\n              layout\n              onClick={() => handleTagClick(tag)}\n              transition={\n                shouldReduceMotion\n                  ? { duration: 0 }\n                  : { duration: 0.25, bounce: 0, 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","target":"components/smoothui/animated-tags/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"animated-toggle","type":"registry:ui","title":"animated-toggle","description":"Animated toggle switch with morph and icon variants","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport { cn } from \"@repo/shadcn-ui/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  type: \"spring\" as const,\n  duration: 0.25,\n  bounce: 0.1,\n};\n\nconst SIZES = {\n  sm: {\n    track: \"w-9 h-5\",\n    thumb: \"size-4\",\n    thumbTranslate: 16,\n    icon: \"size-2.5\",\n  },\n  md: {\n    track: \"w-11 h-6\",\n    thumb: \"size-5\",\n    thumbTranslate: 20,\n    icon: \"size-3\",\n  },\n  lg: {\n    track: \"w-[52px] h-7\",\n    thumb: \"size-6\",\n    thumbTranslate: 24,\n    icon: \"size-3.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                x: getThumbTransform(),\n                borderRadius: getThumbBorderRadius(),\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, scale: 1, rotate: 0 }\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, scale: 0.5, rotate: -90 }\n              }\n              initial={\n                shouldReduceMotion\n                  ? { opacity: 0 }\n                  : { opacity: 0, scale: 0.5, rotate: 90 }\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","target":"components/smoothui/animated-toggle/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"animated-tooltip","type":"registry:ui","title":"animated-tooltip","description":"Spring-animated tooltip with multiple placement options","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport { cn } from \"@repo/shadcn-ui/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  type: \"spring\" as const,\n  duration: 0.25,\n  bounce: 0.1,\n};\n\nconst placementStyles: Record<AnimatedTooltipPlacement, string> = {\n  top: \"bottom-full left-1/2 -translate-x-1/2 mb-2\",\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};\n\nconst arrowStyles: Record<AnimatedTooltipPlacement, string> = {\n  top: \"top-full left-1/2 -translate-x-1/2 border-t-foreground border-x-transparent border-b-transparent\",\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};\n\nconst arrowBorderSize: Record<AnimatedTooltipPlacement, string> = {\n  top: \"border-4\",\n  bottom: \"border-4\",\n  left: \"border-4\",\n  right: \"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        )}\n      </AnimatePresence>\n    </span>\n  );\n};\n\nexport default AnimatedTooltip;\n","target":"components/smoothui/animated-tooltip/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"app-download-stack","type":"registry:ui","title":"app-download-stack","description":"A AppDownloadStack component for SmoothUI.","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion","lucide-react"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","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    id: 1,\n    name: \"GitHub\",\n    icon: \"https://parsefiles.back4app.com/JPaQcFfEEQ1ePBxbf6wvzkPMEqKYHhPYv8boI1Rc/9c9721583ecba33e59ebcebdca2248fd_Mmr12FRh5V.png\",\n  },\n  {\n    id: 2,\n    name: \"Canary\",\n    icon: \"https://parsefiles.back4app.com/JPaQcFfEEQ1ePBxbf6wvzkPMEqKYHhPYv8boI1Rc/b47f43e02f04563447fa90d4ff6c8943_9KzW5GTggQ.png\",\n  },\n  {\n    id: 3,\n    name: \"Figma\",\n    icon: \"https://parsefiles.back4app.com/JPaQcFfEEQ1ePBxbf6wvzkPMEqKYHhPYv8boI1Rc/f0b9cdefa67b57eeb080278c2f6984cc_sCqUJBg6Qq.png\",\n  },\n  {\n    id: 4,\n    name: \"Arc\",\n    icon: \"https://parsefiles.back4app.com/JPaQcFfEEQ1ePBxbf6wvzkPMEqKYHhPYv8boI1Rc/178c7b02003c933e6b5afe98bbee595b_low_res_Arc_Browser.png\",\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        x: [\"0%\", \"100%\"],\n        transition: {\n          duration: 1,\n          repeat: Number.POSITIVE_INFINITY,\n          ease: \"linear\",\n        },\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      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      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      float: (i: number) =>\n        shouldReduceMotion\n          ? { y: 0 }\n          : {\n              y: [0, -FLOAT_AMPLITUDE, 0],\n              transition: {\n                y: {\n                  repeat: Number.POSITIVE_INFINITY,\n                  duration: FLOAT_DURATION,\n                  ease: [0.645, 0.045, 0.355, 1],\n                  delay: i * FLOAT_DELAY_MULTIPLIER,\n                },\n              },\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                  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                        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                    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          )}\n        </AnimatePresence>\n      </motion.div>\n    </div>\n  );\n}\n","target":"components/smoothui/app-download-stack/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"apple-invites","type":"registry:ui","title":"apple-invites","description":"A AppleInvites component for SmoothUI.","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["@smoothui/data","motion","lucide-react","popmotion"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","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  sm: 640,\n  md: 768,\n  lg: 1024,\n  xl: 1280,\n  \"2xl\": 1536,\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        x: \"-50%\",\n        rotate: 0,\n        scale: 1,\n        opacity: 1,\n        zIndex: 3,\n        transition: shouldReduceMotion\n          ? { duration: 0 }\n          : {\n              type: \"spring\" as const,\n              stiffness: 300,\n              damping: 30,\n              duration: 0.25,\n            },\n      },\n      left: {\n        x: \"-130%\",\n        rotate: -12,\n        scale: 0.9,\n        opacity: 0.8,\n        zIndex: 2,\n        transition: shouldReduceMotion\n          ? { duration: 0 }\n          : {\n              type: \"spring\" as const,\n              stiffness: 300,\n              damping: 30,\n              duration: 0.25,\n            },\n      },\n      right: {\n        x: \"30%\",\n        rotate: 12,\n        scale: 0.9,\n        opacity: 0.8,\n        zIndex: 2,\n        transition: shouldReduceMotion\n          ? { duration: 0 }\n          : {\n              type: \"spring\" as const,\n              stiffness: 300,\n              damping: 30,\n              duration: 0.25,\n            },\n      },\n      hidden: {\n        opacity: 0,\n        zIndex: 1,\n        transition: shouldReduceMotion ? { duration: 0 } : { duration: 0.3 },\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          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              width: responsiveWidth,\n              height: responsiveHeight,\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                  top: `${badgeTop}px`,\n                  left: `${badgeLeft}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                    paddingLeft: `${badgePaddingX}px`,\n                    paddingRight: `${badgePaddingX}px`,\n                    paddingTop: `${badgePaddingY}px`,\n                    paddingBottom: `${badgePaddingY}px`,\n                    gap: `${badgeGap}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                    marginBottom: `${avatarMarginBottom}px`,\n                    gap: `${avatarGap}px`,\n                  }}\n                >\n                  {event.participants?.map((participant, idx) => (\n                    <img\n                      alt={`Participant ${idx + 1}`}\n                      className=\"rounded-full\"\n                      height={avatarSize}\n                      key={`participant-${participant.avatar}-${idx}`}\n                      src={participant.avatar}\n                      style={{\n                        width: `${avatarSize}px`,\n                        height: `${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                )}\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                )}\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","target":"components/smoothui/apple-invites/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"basic-accordion","type":"registry:ui","title":"basic-accordion","description":"A BasicAccordion component for SmoothUI.","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion","lucide-react"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport { ChevronDown } from \"lucide-react\";\nimport { AnimatePresence, 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 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-primary 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            <AnimatePresence initial={false}>\n              {isExpanded && (\n                <motion.div\n                  animate={\n                    shouldReduceMotion\n                      ? { height: \"auto\", opacity: 1 }\n                      : {\n                          height: \"auto\",\n                          opacity: 1,\n                          transition: {\n                            height: {\n                              type: \"spring\" as const,\n                              stiffness: 500,\n                              damping: 40,\n                              duration: 0.25,\n                            },\n                            opacity: { duration: 0.2 },\n                          },\n                        }\n                  }\n                  aria-labelledby={`accordion-header-${item.id}`}\n                  className=\"overflow-hidden\"\n                  exit={\n                    shouldReduceMotion\n                      ? { height: 0, opacity: 0, transition: { duration: 0 } }\n                      : {\n                          height: 0,\n                          opacity: 0,\n                          transition: {\n                            height: { duration: 0.2 },\n                            opacity: { duration: 0.15 },\n                          },\n                        }\n                  }\n                  id={`accordion-content-${item.id}`}\n                  initial={\n                    shouldReduceMotion\n                      ? { height: \"auto\", opacity: 1 }\n                      : { height: 0, opacity: 0 }\n                  }\n                  role=\"region\"\n                >\n                  <div className=\"border-t bg-background px-4 py-3\">\n                    {item.content}\n                  </div>\n                </motion.div>\n              )}\n            </AnimatePresence>\n          </div>\n        );\n      })}\n    </div>\n  );\n}\n","target":"components/smoothui/basic-accordion/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"basic-dropdown","type":"registry:ui","title":"basic-dropdown","description":"A BasicDropdown component for SmoothUI.","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion","lucide-react"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","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({ top: 0, left: 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        top: rect.bottom + DROPDOWN_OFFSET,\n        left: rect.left,\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          top: rect.bottom + DROPDOWN_OFFSET,\n          left: rect.left,\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, y: 0, scaleY: 1 }\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                    y: -10,\n                    scaleY: 0.8,\n                    transition: { duration: 0.15 },\n                  }\n            }\n            initial={\n              shouldReduceMotion\n                ? { opacity: 1 }\n                : { opacity: 0, y: -10, scaleY: 0.8 }\n            }\n            style={{\n              top: `${position.top}px`,\n              left: `${position.left}px`,\n              width: `${position.width}px`,\n            }}\n            transition={\n              shouldReduceMotion\n                ? { duration: 0 }\n                : { type: \"spring\" as const, bounce: 0.1, duration: 0.25 }\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                          type: \"spring\" as const,\n                          stiffness: 300,\n                          damping: 30,\n                          duration: 0.2,\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-primary 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 && <span className=\"mr-2\">{item.icon}</span>}\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                                type: \"spring\" as const,\n                                stiffness: 300,\n                                damping: 20,\n                                duration: 0.2,\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      )}\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 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-primary 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            {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","target":"components/smoothui/basic-dropdown/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"basic-modal","type":"registry:ui","title":"basic-modal","description":"A BasicModal component for SmoothUI.","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion","lucide-react","usehooks-ts"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","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  sm: \"max-w-sm\",\n  md: \"max-w-md\",\n  lg: \"max-w-lg\",\n  xl: \"max-w-xl\",\n  full: \"max-w-4xl\",\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[0];\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 ? {} : { scale: 1, y: 0, opacity: 1 }}\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                      scale: 0.95,\n                      y: 10,\n                      opacity: 0,\n                      transition: { duration: 0.15 },\n                    }\n              }\n              initial={\n                shouldReduceMotion\n                  ? { opacity: 1 }\n                  : { scale: 0.95, y: 10, opacity: 0 }\n              }\n              ref={modalRef}\n              role=\"dialog\"\n              transition={\n                shouldReduceMotion\n                  ? { duration: 0 }\n                  : {\n                      type: \"spring\" as const,\n                      damping: 25,\n                      stiffness: 300,\n                      duration: 0.25,\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                )}\n                <motion.button\n                  aria-label=\"Close modal\"\n                  className=\"ml-auto min-h-[44px] min-w-[44px] rounded-full p-2 transition-colors hover:bg-secondary focus-visible:ring-2 focus-visible:ring-primary 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      )}\n    </AnimatePresence>\n  );\n\n  if (!mounted) {\n    return null;\n  }\n\n  return createPortal(modalContent, document.body);\n}\n","target":"components/smoothui/basic-modal/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"basic-toast","type":"registry:ui","title":"basic-toast","description":"A BasicToast component for SmoothUI.","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion","lucide-react"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","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  success: <CheckCircle className=\"h-5 w-5 text-emerald-500\" />,\n  error: <XCircle className=\"h-5 w-5 text-red-500\" />,\n  warning: <AlertCircle className=\"h-5 w-5 text-amber-500\" />,\n  info: <Info className=\"h-5 w-5 text-blue-500\" />,\n};\n\nconst toastClasses = {\n  success:\n    \"border-emerald-100 bg-emerald-50 dark:border-emerald-900 dark:bg-emerald-950\",\n  error: \"border-red-100 bg-red-50 dark:border-red-900 dark:bg-red-950\",\n  warning:\n    \"border-amber-100 bg-amber-50 dark:border-amber-900 dark:bg-amber-950\",\n  info: \"border-blue-100 bg-blue-50 dark:border-blue-900 dark:bg-blue-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, x: 0, scale: 1 }\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                  x: 50,\n                  scale: 0.8,\n                  transition: { duration: 0.15 },\n                }\n          }\n          initial={\n            shouldReduceMotion\n              ? { opacity: 1 }\n              : { opacity: 0, x: 50, scale: 0.8 }\n          }\n          transition={\n            shouldReduceMotion\n              ? { duration: 0 }\n              : { type: \"spring\" as const, bounce: 0.1, duration: 0.25 }\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 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      )}\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=\"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=\"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=\"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=\"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        )}\n      </AnimatePresence>\n    </div>\n  );\n}\n","target":"components/smoothui/basic-toast/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"book","type":"registry:ui","title":"book","description":"A 3D CSS book component with perspective transforms and hover animation","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport { cn } from \"@repo/shadcn-ui/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]:\n          textColor || \"var(--color-fd-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            width: bookWidth,\n            height: \"100%\",\n            borderRadius: BOOK_BORDER_RADIUS,\n            background:\n              variant === \"simple\" ? color : \"var(--color-fd-background)\",\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            width: `calc(${BOOK_DEPTH} - 2px)`,\n            top: \"3px\",\n            transform: `translateX(calc(${bookWidth} - ${BOOK_DEPTH} / 2 - 3px)) rotateY(90deg) translateX(calc(${BOOK_DEPTH} / 2))`,\n          }}\n        />\n\n        {/* Back cover */}\n        <div\n          aria-hidden\n          className=\"pointer-events-none absolute left-0\"\n          style={{\n            width: bookWidth,\n            height: \"100%\",\n            borderRadius: BOOK_BORDER_RADIUS,\n            transform: `translateZ(calc(-1 * ${BOOK_DEPTH}))`,\n            backgroundColor:\n              variant === \"simple\" ? color : \"var(--color-fd-background)\",\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          padding: \"6.1%\",\n          gap: `${(24 / DEFAULT_WIDTH) * width}px`,\n        }}\n      >\n        <span\n          className=\"text-balance font-semibold leading-[1.25em] tracking-[-0.02em]\"\n          style={{\n            fontSize: \"10.5cqw\",\n            color: \"var(--book-text-color)\",\n          }}\n        >\n          {title}\n        </span>\n        {logo && <div>{logo}</div>}\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        padding: \"6.1%\",\n        gap: `${(16 / DEFAULT_WIDTH) * width}px`,\n      }}\n    >\n      <span\n        className=\"text-balance font-semibold leading-[1.25em] tracking-[-0.02em]\"\n        style={{\n          fontSize: \"12cqw\",\n          color: \"var(--book-text-color)\",\n        }}\n      >\n        {title}\n      </span>\n      {logo && <div>{logo}</div>}\n    </div>\n  </div>\n);\n\nexport default Book;\n","target":"components/smoothui/book/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"breadcrumb","type":"registry:ui","title":"breadcrumb","description":"Animated breadcrumb navigation with stagger-in animation","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion","lucide-react"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport { cn } from \"@repo/shadcn-ui/lib/utils\";\nimport { ChevronRight } from \"lucide-react\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport type { ReactNode } from \"react\";\nimport { SPRING_DEFAULT } from \"../../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","target":"components/smoothui/breadcrumb/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"button-copy","type":"registry:ui","title":"button-copy","description":"A ButtonCopy component for SmoothUI.","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion","lucide-react"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","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-primary 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                : { opacity: 1, y: 0, filter: \"blur(0px)\" }\n            }\n            className=\"flex w-full items-center justify-center\"\n            exit={\n              shouldReduceMotion\n                ? { opacity: 0, transition: { duration: 0 } }\n                : { opacity: 0, y: 25, filter: \"blur(10px)\" }\n            }\n            initial={\n              shouldReduceMotion\n                ? { opacity: 1 }\n                : { opacity: 0, y: -25, filter: \"blur(10px)\" }\n            }\n            key={buttonState}\n            transition={\n              shouldReduceMotion\n                ? { duration: 0 }\n                : { type: \"spring\" as const, duration: 0.25, bounce: 0 }\n            }\n          >\n            {icons[buttonState]}\n          </motion.span>\n        </AnimatePresence>\n      </button>\n    </div>\n  );\n}\n","target":"components/smoothui/button-copy/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"checkbox","type":"registry:ui","title":"checkbox","description":"An animated Checkbox component for SmoothUI with checkmark and indeterminate state animations.","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion","radix-ui"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport { cn } from \"@repo/shadcn-ui/lib/utils\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { Checkbox as CheckboxPrimitive } from \"radix-ui\";\nimport { SPRING_DEFAULT } from \"../../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","target":"components/smoothui/checkbox/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"clip-corners-button","type":"registry:ui","title":"clip-corners-button","description":"A ClipCornersButton component for SmoothUI.","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion"],"devDependencies":[],"registryDependencies":["https://smoothui.dev/r/smooth-button.json"],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport SmoothButton from \"@repo/smoothui/components/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={{ width: 8, height: 8 }}\n        transition={\n          shouldReduceMotion\n            ? { duration: 0 }\n            : {\n                type: \"spring\" as const,\n                stiffness: 400,\n                damping: 24,\n                duration: 0.2,\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={{ width: 8, height: 8 }}\n        transition={\n          shouldReduceMotion\n            ? { duration: 0 }\n            : {\n                type: \"spring\" as const,\n                stiffness: 400,\n                damping: 24,\n                duration: 0.2,\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={{ width: 8, height: 8 }}\n        transition={\n          shouldReduceMotion\n            ? { duration: 0 }\n            : {\n                type: \"spring\" as const,\n                stiffness: 400,\n                damping: 24,\n                duration: 0.2,\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={{ width: 8, height: 8 }}\n        transition={\n          shouldReduceMotion\n            ? { duration: 0 }\n            : {\n                type: \"spring\" as const,\n                stiffness: 400,\n                damping: 24,\n                duration: 0.2,\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","target":"components/smoothui/clip-corners-button/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"combobox","type":"registry:ui","title":"combobox","description":"An animated Combobox component for SmoothUI with text filtering, async search, and keyboard navigation.","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion","lucide-react"],"devDependencies":[],"registryDependencies":["https://smoothui.dev/r/smooth-button.json","https://smoothui.dev/r/smooth-button.json"],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport {\n  Command,\n  CommandEmpty,\n  CommandGroup,\n  CommandInput,\n  CommandItem,\n  CommandList,\n} from \"@repo/shadcn-ui/components/ui/command\";\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@repo/shadcn-ui/components/ui/popover\";\nimport { cn } from \"@repo/shadcn-ui/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 \"../../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              )}\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","target":"components/smoothui/combobox/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"context-menu","type":"registry:ui","title":"context-menu","description":"An animated Context Menu component for SmoothUI with spring animations, nested submenus, and keyboard navigation.","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","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 \"@repo/shadcn-ui/components/ui/context-menu\";\nimport { cn } from \"@repo/shadcn-ui/lib/utils\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport type React from \"react\";\nimport { SPRING_DEFAULT } from \"../../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>}\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>}\n          {item.label}\n          {item.shortcut && (\n            <ContextMenuShortcut>{item.shortcut}</ContextMenuShortcut>\n          )}\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","target":"components/smoothui/context-menu/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"contribution-graph","type":"registry:ui","title":"contribution-graph","description":"A ContributionGraph component for SmoothUI.","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","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\")[0];\n  const existingData = contributionData.find((d) => d.date === dateString);\n  return {\n    date: dateString,\n    count: existingData?.count ?? LEVEL_0,\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          currentYear,\n          targetYear,\n          currentMonth,\n          startDateDay: startDate.getDay(),\n          weekCount,\n        })\n      ) {\n        headers.push({\n          month: MONTHS[currentMonth],\n          colspan: weekCount,\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      currentYear,\n      targetYear,\n      currentMonth,\n      startDateDay: startDate.getDay(),\n      weekCount,\n    })\n  ) {\n    headers.push({\n      month: MONTHS[currentMonth],\n      colspan: weekCount,\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            date: \"\",\n            count: LEVEL_0,\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      weekday: \"long\",\n      year: \"numeric\",\n      month: \"long\",\n      day: \"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 }, (_, w) => {\n                  const dayData = yearData[w * DAYS_IN_WEEK + dayIndex];\n                  const cellKey = `${dayData?.date ?? \"empty\"}-${w}-${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      )}\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      )}\n    </div>\n  );\n}\n\nexport default ContributionGraph;\n","target":"components/smoothui/contribution-graph/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"cursor-follow","type":"registry:ui","title":"cursor-follow","description":"A CursorFollow component for SmoothUI.","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","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, { stiffness: 350, damping: 40 });\n  const springY = useSpring(y, { stiffness: 350, damping: 40 });\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={{ minHeight: 300, cursor: \"none\" }}\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                  width: bubbleWidth,\n                  height: 40,\n                  borderRadius: 20,\n                  background: \"var(--color-brand, #6366f1)\",\n                  color: \"#fff\",\n                  paddingLeft: 16,\n                  paddingRight: 16,\n                  minWidth: 40,\n                  minHeight: 32,\n                  scale: 1.1,\n                }\n              : {\n                  width: CIRCLE_SIZE,\n                  height: CIRCLE_SIZE,\n                  borderRadius: 999,\n                  background: \"var(--color-brand, #6366f1)\",\n                  color: \"#fff\",\n                  paddingLeft: 0,\n                  paddingRight: 0,\n                  minWidth: CIRCLE_SIZE,\n                  minHeight: CIRCLE_SIZE,\n                  scale: 1,\n                }\n          }\n          className=\"flex items-center justify-center font-medium text-xs shadow-lg\"\n          layout\n          style={{\n            display: \"flex\",\n            alignItems: \"center\",\n            justifyContent: \"center\",\n            position: \"relative\",\n            zIndex: 1,\n            boxShadow: \"0 2px 8px 0 rgba(0,0,0,0.10)\",\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={{ opacity: 1, filter: \"blur(0px)\" }}\n              exit={{ opacity: 0, filter: \"blur(8px)\" }}\n              initial={{ opacity: 0, filter: \"blur(8px)\" }}\n              style={{\n                whiteSpace: \"nowrap\",\n                width: \"100%\",\n                textAlign: \"center\",\n                color: \"#fff\",\n              }}\n              transition={\n                shouldReduceMotion\n                  ? { duration: 0 }\n                  : {\n                      duration: 0.2,\n                      delay: 0.05,\n                      ease: [0.645, 0.045, 0.355, 1],\n                    }\n              }\n            >\n              {cursorText}\n            </motion.span>\n          )}\n        </motion.div>\n        {/* Hidden span for pre-measuring text width */}\n        {(pendingText || cursorText) && (\n          <span\n            ref={measureRef}\n            style={{\n              position: \"absolute\",\n              visibility: \"hidden\",\n              pointerEvents: \"none\",\n              whiteSpace: \"nowrap\",\n              fontSize: \"0.75rem\",\n              fontWeight: 500,\n              paddingLeft: 16,\n              paddingRight: 16,\n              fontFamily: \"inherit\",\n            }}\n          >\n            {pendingText || cursorText}\n          </span>\n        )}\n      </motion.div>\n    </div>\n  );\n};\n\nexport default CursorFollow;\n","target":"components/smoothui/cursor-follow/index.tsx"},{"type":"registry:ui","path":"use-cursor-position.tsx","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","target":"components/smoothui/cursor-follow/use-cursor-position.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"dialog","type":"registry:ui","title":"dialog","description":"Animated Dialog and AlertDialog components for SmoothUI.","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["lucide-react","motion","radix-ui"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport {\n  AlertDialogAction,\n  AlertDialogCancel,\n  AlertDialogDescription,\n  AlertDialogFooter,\n  AlertDialogHeader,\n  AlertDialogTitle,\n  AlertDialogTrigger,\n} from \"@repo/shadcn-ui/components/ui/alert-dialog\";\nimport {\n  DialogClose,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from \"@repo/shadcn-ui/components/ui/dialog\";\nimport { cn } from \"@repo/shadcn-ui/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  type: \"spring\" as const,\n  duration: 0.25,\n  bounce: 0.05,\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            type: \"spring\" as const,\n            duration: 0.25,\n            bounce: 0,\n            delay: index * STAGGER_DELAY,\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 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>}\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>}\n                      {description && (\n                        <DialogDescription>{description}</DialogDescription>\n                      )}\n                    </DialogHeader>\n                  </StaggerChild>\n                )}\n\n                {children && (\n                  <StaggerChild\n                    index={title || description ? 1 : 0}\n                    shouldReduceMotion={shouldReduceMotion}\n                  >\n                    {children}\n                  </StaggerChild>\n                )}\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                )}\n\n                {showCloseButton && (\n                  <AnimatedCloseButton\n                    onClick={() => handleOpenChange(false)}\n                    shouldReduceMotion={shouldReduceMotion}\n                  />\n                )}\n              </motion.div>\n            </DialogRadix.Content>\n          </DialogRadix.Portal>\n        )}\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 && <AlertDialogTrigger asChild>{trigger}</AlertDialogTrigger>}\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 && <AlertDialogTitle>{title}</AlertDialogTitle>}\n                      {description && (\n                        <AlertDialogDescription>\n                          {description}\n                        </AlertDialogDescription>\n                      )}\n                    </AlertDialogHeader>\n                  </StaggerChild>\n                )}\n\n                {children && (\n                  <StaggerChild\n                    index={title || description ? 1 : 0}\n                    shouldReduceMotion={shouldReduceMotion}\n                  >\n                    {children}\n                  </StaggerChild>\n                )}\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                )}\n              </motion.div>\n            </AlertDialogRadix.Content>\n          </AlertDialogRadix.Portal>\n        )}\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","target":"components/smoothui/dialog/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"dot-morph-button","type":"registry:ui","title":"dot-morph-button","description":"A DotMorphButton component for SmoothUI.","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion"],"devDependencies":[],"registryDependencies":["https://smoothui.dev/r/smooth-button.json"],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport SmoothButton from \"@repo/smoothui/components/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                width: 16,\n                height: 16,\n                borderRadius: 999,\n              }\n            : {\n                width: 12,\n                height: 28,\n                borderRadius: 999,\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                type: \"spring\" as const,\n                stiffness: 600,\n                damping: 22,\n                duration: 0.25,\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","target":"components/smoothui/dot-morph-button/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"drawer","type":"registry:ui","title":"drawer","description":"An animated Drawer component for SmoothUI that slides from any side.","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","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 \"@repo/shadcn-ui/components/ui/drawer\";\nimport { cn } from \"@repo/shadcn-ui/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            type: \"spring\" as const,\n            duration: 0.25,\n            bounce: 0,\n            delay: STAGGER_BASE_DELAY + index * STAGGER_CHILD_DELAY,\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            repeat: Number.POSITIVE_INFINITY,\n            ease: [0.645, 0.045, 0.355, 1],\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>}\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>}\n                    {description && (\n                      <DrawerDescription>{description}</DrawerDescription>\n                    )}\n                  </DrawerHeader>\n                </StaggerChild>\n              )}\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              )}\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              )}\n            </motion.div>\n          )}\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","target":"components/smoothui/drawer/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"dropdown-menu","type":"registry:ui","title":"dropdown-menu","description":"An animated Dropdown Menu component for SmoothUI with spring animations, nested submenus, and keyboard navigation.","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","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 \"@repo/shadcn-ui/components/ui/dropdown-menu\";\nimport { cn } from \"@repo/shadcn-ui/lib/utils\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport type React from \"react\";\nimport { useState } from \"react\";\nimport { SPRING_DEFAULT } from \"../../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>}\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>}\n          {item.label}\n          {item.shortcut && (\n            <DropdownMenuShortcut>{item.shortcut}</DropdownMenuShortcut>\n          )}\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","target":"components/smoothui/dropdown-menu/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"dynamic-island","type":"registry:ui","title":"dynamic-island","description":"A DynamicIsland component for SmoothUI.","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion","lucide-react"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","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  \"ring-idle\": 0.5,\n  \"timer-ring\": 0.35,\n  \"ring-timer\": 0.35,\n  \"timer-idle\": 0.3,\n  \"idle-timer\": 0.3,\n  \"idle-ring\": 0.5,\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-foreground\"\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        )}\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-foreground\">\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-foreground\">\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-foreground\">\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-foreground\">\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                  type: \"spring\" as const,\n                  bounce:\n                    BOUNCE_VARIANTS[\n                      variantKey as keyof typeof BOUNCE_VARIANTS\n                    ] ?? DEFAULT_BOUNCE,\n                  duration: 0.25,\n                }\n          }\n        >\n          <motion.div\n            animate={\n              shouldReduceMotion\n                ? { scale: 1, opacity: 1 }\n                : {\n                    scale: 1,\n                    opacity: 1,\n                    filter: \"blur(0px)\",\n                    originX: 0.5,\n                    originY: 0.5,\n                    transition: { delay: 0.05 },\n                  }\n            }\n            initial={{\n              scale: 0.9,\n              opacity: 0,\n              filter: \"blur(5px)\",\n              originX: 0.5,\n              originY: 0.5,\n            }}\n            key={view}\n            transition={{\n              type: \"spring\" as const,\n              bounce:\n                BOUNCE_VARIANTS[variantKey as keyof typeof BOUNCE_VARIANTS] ??\n                DEFAULT_BOUNCE,\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            { key: \"idle\", icon: <CloudLightning className=\"size-3\" /> },\n            { key: \"ring\", icon: <Phone className=\"size-3\" /> },\n            { key: \"timer\", icon: <TimerIcon className=\"size-3\" /> },\n            { key: \"notification\", icon: <Bell className=\"size-3\" /> },\n            { key: \"music\", icon: <Music2 className=\"size-3\" /> },\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","target":"components/smoothui/dynamic-island/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"expandable-cards","type":"registry:ui","title":"expandable-cards","description":"A ExpandableCards component for SmoothUI.","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion","lucide-react"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","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.scrollWidth;\n      const clientWidth = scrollRef.current.clientWidth;\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          scrollSnapType: \"x mandatory\",\n          scrollPaddingLeft: \"20%\",\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-primary 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                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-primary 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                      ? { width: \"300px\", opacity: 1 }\n                      : { width: \"300px\", opacity: 1, filter: \"blur(0px)\" }\n                  }\n                  className=\"absolute top-0 right-0 h-full bg-background\"\n                  exit={\n                    shouldReduceMotion\n                      ? { width: 0, opacity: 0 }\n                      : { width: 0, opacity: 0, filter: \"blur(5px)\" }\n                  }\n                  initial={\n                    shouldReduceMotion\n                      ? { width: 0, opacity: 0 }\n                      : { width: 0, opacity: 0, filter: \"blur(5px)\" }\n                  }\n                  transition={\n                    shouldReduceMotion\n                      ? { duration: 0 }\n                      : {\n                          duration: 0.25,\n                          ease: EASE_OUT_QUINT,\n                          opacity: { duration: 0.2, delay: 0.1 },\n                        }\n                  }\n                >\n                  <motion.div\n                    animate={\n                      shouldReduceMotion\n                        ? { opacity: 1, x: 0 }\n                        : { opacity: 1, x: 0, filter: \"blur(0px)\" }\n                    }\n                    className=\"flex h-full flex-col justify-between p-8\"\n                    exit={\n                      shouldReduceMotion\n                        ? { opacity: 0, x: 20 }\n                        : { opacity: 0, x: 20, filter: \"blur(5px)\" }\n                    }\n                    initial={\n                      shouldReduceMotion\n                        ? { opacity: 0, x: 20 }\n                        : { opacity: 0, x: 20, filter: \"blur(5px)\" }\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                            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                    )}\n                  </motion.div>\n                </motion.div>\n              )}\n            </AnimatePresence>\n          </motion.div>\n        ))}\n      </div>\n    </div>\n  );\n}\n","target":"components/smoothui/expandable-cards/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"exposure-slider","type":"registry:ui","title":"exposure-slider","description":"iOS-style exposure slider with draggable ticker and progress ring","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport { cn } from \"@repo/shadcn-ui/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 = { stiffness: 300, damping: 30, mass: 0.5 };\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      )}\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            touchAction: \"pan-y\",\n            padding: `0 calc(50% - ${NOTCH_WIDTH / 2}px)`,\n          }}\n        >\n          <motion.ul\n            className=\"relative m-0 flex h-full list-none items-center p-0\"\n            style={{ x, marginLeft: -centerIndex * NOTCH_WIDTH }}\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            width: 3,\n            height: 40,\n            backgroundColor: bg,\n            clipPath,\n            opacity,\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            transformOrigin: \"50% 50%\",\n            transformBox: \"fill-box\",\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            transformOrigin: \"50% 50%\",\n            transformBox: \"fill-box\",\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","target":"components/smoothui/exposure-slider/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"figma-comment","type":"registry:ui","title":"figma-comment","description":"A FigmaComment component for SmoothUI.","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport {\n  Avatar,\n  AvatarFallback,\n  AvatarImage,\n} from \"@repo/shadcn-ui/components/ui/avatar\";\nimport { cn } from \"@repo/shadcn-ui/lib/utils\";\nimport { getImageKitUrl } from \"@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      width: 48,\n      height: 48,\n      quality: 85,\n      format: \"auto\",\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                width: isOpen ? width : CLOSED_SIZE,\n                height: isOpen ? contentHeight : 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                width: isOpen ? width : CLOSED_SIZE,\n                height: isOpen ? contentHeight : CLOSED_SIZE,\n              }\n            : undefined\n        }\n        transition={\n          shouldReduceMotion\n            ? { duration: 0 }\n            : {\n                type: \"spring\" as const,\n                stiffness: 550,\n                damping: 45,\n                mass: 0.7,\n                delay: isOpen ? 0 : CONTAINER_CLOSE_DELAY,\n                duration: 0.25,\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                  type: \"spring\" as const,\n                  stiffness: 300,\n                  damping: 25,\n                  duration: 0.25,\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            width: `${width}px`,\n            top: \"-9999px\",\n            left: 0,\n            position: \"absolute\",\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                      opacity: 1,\n                      filter: \"blur(0px)\",\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                      opacity: 0,\n                      filter: `blur(${String(EXIT_BLUR_PX)}px)`,\n                    }\n              }\n              initial={\n                shouldReduceMotion\n                  ? { opacity: 0 }\n                  : {\n                      opacity: 0,\n                      filter: `blur(${String(INITIAL_BLUR_PX)}px)`,\n                    }\n              }\n              style={{\n                width: `${width}px`,\n              }}\n              transition={\n                (shouldReduceMotion\n                  ? { duration: 0 }\n                  : (isExiting: boolean) => ({\n                      opacity: {\n                        duration: 0.25,\n                        ease: BLUR_EASE,\n                        delay: isExiting ? 0 : CONTENT_DELAY,\n                      },\n                      filter: {\n                        duration: 0.25,\n                        ease: BLUR_EASE,\n                        delay: isExiting ? 0 : CONTENT_DELAY,\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          )}\n        </AnimatePresence>\n      </motion.div>\n    </div>\n  );\n}\n","target":"components/smoothui/figma-comment/index.tsx"},{"type":"registry:ui","path":"use-click-outside.tsx","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","target":"components/smoothui/figma-comment/use-click-outside.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"form","type":"registry:ui","title":"form","description":"A lightweight, composable Form component with animated error messages and full accessibility support.","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["lucide-react","motion"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport { cn } from \"@repo/shadcn-ui/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 \"../../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  submitCount: 0,\n  prevErrors: {},\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, submitCount, prevErrors }),\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      name,\n      id,\n      error,\n      formItemId: `${id}-form-item`,\n      formDescriptionId: `${id}-form-item-description`,\n      formMessageId: `${id}-form-item-message`,\n      fieldIndex: fieldIndexRef.current ?? 0,\n      submitCount,\n      prevError,\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        id: formItemId,\n        \"aria-describedby\": error\n          ? `${formDescriptionId} ${formMessageId}`\n          : formDescriptionId,\n        \"aria-invalid\": error ? true : undefined,\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                    type: \"spring\" as const,\n                    stiffness: 300,\n                    damping: 20,\n                    duration: 0.25,\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                      type: \"spring\" as const,\n                      stiffness: 400,\n                      damping: 15,\n                      duration: 0.2,\n                      delay: 0.05,\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","target":"components/smoothui/form/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"github-stars-animation","type":"registry:ui","title":"github-stars-animation","description":"A GitHubStarsAnimation component for SmoothUI.","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion","lucide-react"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport { cn } from \"@repo/shadcn-ui/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    stiffness: 100,\n    damping: 30,\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                      duration: TRANSITION_DURATION,\n                      delay: index * STAGGER_DELAY,\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                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","target":"components/smoothui/github-stars-animation/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"glow-hover-card","type":"registry:ui","title":"glow-hover-card","description":"A GlowHoverCards component for SmoothUI.","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport { cn } from \"@repo/shadcn-ui/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  }>({ x: 0, y: 0, opacity: 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        x,\n        y,\n        opacity: 1,\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        childList: true,\n        subtree: true,\n        attributes: 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        borderColor: `hsla(${hsl}, 1)`,\n        boxShadow: `0 0 0 1px inset hsl(${hsl}), 0 0 20px hsla(${hsl}, ${glowIntensity})`,\n        backgroundColor: `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        borderColor: brandColor,\n        boxShadow: `0 0 0 1px inset ${brandColor}, 0 0 20px ${brandWithOpacity}`,\n        backgroundColor: 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      style: mergedStyle,\n      className: cn(existingClassName, \"glow-overlay-item\"),\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            opacity: mousePosition.opacity,\n            maskImage: `radial-gradient(${maskSize}px ${maskSize}px at ${mousePosition.x}px ${mousePosition.y}px, #000 1%, transparent 50%)`,\n            WebkitMaskImage: `radial-gradient(${maskSize}px ${maskSize}px at ${mousePosition.x}px ${mousePosition.y}px, #000 1%, transparent 50%)`,\n            transition:\n              \"opacity 200ms ease, mask-image 200ms ease, -webkit-mask-image 200ms ease\",\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","target":"components/smoothui/glow-hover-card/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"gooey-popover","type":"registry:ui","title":"gooey-popover","description":"A GooeyPopover component for SmoothUI.","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["gsap"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport { cn } from \"@repo/shadcn-ui/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          width: contentWidth,\n          height: contentHeight,\n          borderRadius: CONTENT_BORDER_RADIUS,\n          x: contentLeft,\n          y: translateY,\n          opacity: 1,\n        });\n        gsap.set(innerTarget, { opacity: 1, y: 0 });\n      } else {\n        gsap.set(unfilteredTarget, {\n          width: triggerSize,\n          height: triggerSize,\n          borderRadius: triggerRadius,\n          x: 0,\n          y: 0,\n          opacity: 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        width: triggerSize,\n        height: triggerSize,\n        borderRadius: triggerRadius,\n        x: 0,\n        y: 0,\n        opacity: 1,\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            width: contentWidth,\n            height: contentHeight,\n            borderRadius: 0,\n            x: contentLeft,\n            y: translateY,\n            duration: speed,\n            ease: \"power1.in\",\n          },\n          0\n        );\n      }\n\n      // Unfiltered content: morph to rectangle with rounded corners\n      tl.to(\n        unfilteredTarget,\n        {\n          width: contentWidth,\n          height: contentHeight,\n          borderRadius: CONTENT_BORDER_RADIUS,\n          x: contentLeft,\n          y: translateY,\n          duration: speed,\n          ease: \"power1.in\",\n        },\n        0\n      );\n\n      // Content text fade in (overlapping with shape morph)\n      tl.to(\n        innerTarget,\n        {\n          opacity: 1,\n          y: 0,\n          duration: speed * 0.75,\n          ease: \"power1.out\",\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        opacity: 0,\n        y: 8,\n        duration: speed * 0.4,\n        ease: \"power1.in\",\n      });\n\n      // Shape morph back: rectangle → circle\n      const targets = [filteredTarget, unfilteredTarget].filter(Boolean);\n      tl.to(\n        targets,\n        {\n          width: triggerSize,\n          height: triggerSize,\n          borderRadius: triggerRadius,\n          x: 0,\n          y: 0,\n          duration: speed,\n          ease: \"power1.in\",\n        },\n        speed * 0.2\n      );\n\n      // Fade out at the end\n      tl.to(\n        targets,\n        {\n          opacity: 0,\n          duration: speed * 0.3,\n          ease: \"power1.in\",\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={{ width: 0, height: 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          width: contentWidth,\n          position: \"absolute\",\n          top: -9999,\n          left: -9999,\n          visibility: \"hidden\",\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              width: triggerSize,\n              height: triggerSize,\n              top: 0,\n              left: 0,\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              top: 0,\n              left: 0,\n              width: triggerSize,\n              height: triggerSize,\n              borderRadius: triggerRadius,\n              opacity: 0,\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 items-center justify-center rounded-full text-white transition-colors\",\n          bgClassName\n        )}\n        onClick={() => setIsOpen(!isOpen)}\n        style={{\n          width: triggerSize,\n          height: 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            top: 0,\n            left: 0,\n            width: triggerSize,\n            height: triggerSize,\n            borderRadius: triggerRadius,\n            opacity: 0,\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      )}\n    </div>\n  );\n}\n","target":"components/smoothui/gooey-popover/index.tsx"},{"type":"registry:ui","path":"use-click-outside.tsx","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","target":"components/smoothui/gooey-popover/use-click-outside.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"grid-loader","type":"registry:ui","title":"grid-loader","description":"3x3 grid-based loading animation with preset patterns","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport { cn } from \"@repo/shadcn-ui/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  // 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  \"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\n  // Horizontal lines\n  \"line-h-top\": [\n    [1, 1, 1],\n    [0, 0, 0],\n    [0, 0, 0],\n  ],\n  \"line-h-mid\": [\n    [0, 0, 0],\n    [1, 1, 1],\n    [0, 0, 0],\n  ],\n  \"line-h-bot\": [\n    [0, 0, 0],\n    [0, 0, 0],\n    [1, 1, 1],\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\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\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  corners: [\n    [1, 0, 1],\n    [0, 0, 0],\n    [1, 0, 1],\n  ],\n\n  // Plus\n  \"plus-hollow\": [\n    [0, 1, 0],\n    [1, 0, 1],\n    [0, 1, 0],\n  ],\n  \"plus-full\": [\n    [0, 1, 0],\n    [1, 1, 1],\n    [0, 1, 0],\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  \"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  // T-shapes\n  \"T-top\": [\n    [1, 1, 1],\n    [0, 1, 0],\n    [0, 0, 0],\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  // 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  \"duo-diag\": [\n    [1, 0, 0],\n    [0, 1, 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\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\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  \"wave-bt\": [\n    [0, 0, 0],\n    [1, 1, 1],\n    [1, 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  \"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  // Ripple\n  \"ripple-out\": [\n    [1, 1, 1],\n    [1, 0, 1],\n    [1, 1, 1],\n  ],\n  \"ripple-in\": [\n    [0, 0, 0],\n    [0, 1, 0],\n    [0, 0, 0],\n  ],\n\n  // Shapes\n  cross: [\n    [0, 1, 0],\n    [1, 1, 1],\n    [0, 1, 0],\n  ],\n  \"x-shape\": [\n    [1, 0, 1],\n    [0, 1, 0],\n    [1, 0, 1],\n  ],\n  diamond: [\n    [0, 1, 0],\n    [1, 0, 1],\n    [0, 1, 0],\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\n  // Patterns\n  checkerboard: [\n    [1, 0, 1],\n    [0, 1, 0],\n    [1, 0, 1],\n  ],\n  \"rows-alt\": [\n    [1, 1, 1],\n    [0, 0, 0],\n    [1, 1, 1],\n  ],\n\n  // Spirals\n  \"spiral-cw\": [\n    [1, 1, 1],\n    [0, 0, 1],\n    [0, 0, 1],\n  ],\n  \"spiral-ccw\": [\n    [1, 1, 1],\n    [1, 0, 0],\n    [1, 0, 0],\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\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\n  // Effects\n  waterfall: [\n    [1, 1, 0],\n    [0, 0, 0],\n    [0, 0, 0],\n  ],\n  breathing: [\n    [0, 0, 0],\n    [0, 1, 0],\n    [0, 0, 0],\n  ],\n  heartbeat: [\n    [0, 1, 0],\n    [1, 1, 1],\n    [0, 1, 0],\n  ],\n  twinkle: [\n    [1, 0, 0],\n    [0, 0, 1],\n    [0, 1, 0],\n  ],\n  sparkle: [\n    [1, 0, 1],\n    [0, 1, 0],\n    [1, 0, 1],\n  ],\n  chaos: [\n    [1, 0, 1],\n    [0, 1, 0],\n    [1, 0, 0],\n  ],\n\n  // Edge\n  \"edge-cw\": [\n    [1, 1, 1],\n    [0, 0, 0],\n    [0, 0, 0],\n  ],\n  border: [\n    [1, 1, 1],\n    [1, 0, 1],\n    [1, 1, 1],\n  ],\n};\n\n// Color presets\nconst COLORS: Record<string, string> = {\n  white: \"#f5f5f4\",\n  red: \"#f87171\",\n  blue: \"#38bdf8\",\n  green: \"#4ade80\",\n  amber: \"#fbbf24\",\n};\n\n// Size presets in pixels\nconst SIZES: Record<string, number> = {\n  sm: 24,\n  md: 32,\n  lg: 48,\n  xl: 64,\n};\n\n// Speed presets in milliseconds\nconst SPEEDS: Record<string, number> = {\n  slow: 1500,\n  normal: 800,\n  fast: 400,\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          width: cellSize,\n          height: cellSize,\n        }}\n      />\n    );\n  }\n\n  // For reduced motion, show static cells\n  if (shouldReduceMotion) {\n    return (\n      <div\n        style={{\n          width: cellSize,\n          height: cellSize,\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        }}\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        width: cellSize,\n        height: cellSize,\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      }}\n      transition={{\n        duration: cycleDuration / 1000,\n        repeat: Number.POSITIVE_INFINITY,\n        ease: [0.645, 0.045, 0.355, 1],\n        delay: mode === \"stagger\" ? animationDelay : 0,\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        width: sizeInPx,\n        height: sizeInPx,\n        gap: gapSize,\n        filter: blur ? `blur(${blur}px)` : undefined,\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","target":"components/smoothui/grid-loader/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"image-metadata-preview","type":"registry:ui","title":"image-metadata-preview","description":"A ImageMetadataPreview component for SmoothUI.","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion","lucide-react","react-use-measure"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","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 alt={alt} height={437} src={imageSrc} width={300} />\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-primary 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-primary 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                    : { opacity: 1, filter: \"blur(0px)\" }\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-primary focus-visible:ring-offset-2 ${\n                  isHoverDevice ? \"hover:bg-muted\" : \"\"\n                }`}\n                initial={\n                  shouldReduceMotion\n                    ? { opacity: 1 }\n                    : { opacity: 0, filter: \"blur(4px)\" }\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                  : { opacity: 1, filter: \"blur(0px)\" }\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                  : { opacity: 0, filter: \"blur(4px)\" }\n              }\n              onClick={handleClickClose}\n              style={{ borderRadius: 20 }}\n              transition={\n                shouldReduceMotion\n                  ? { duration: 0 }\n                  : { type: \"spring\" as const, duration: 0.25, bounce: 0 }\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-primary 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","target":"components/smoothui/image-metadata-preview/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"infinite-slider","type":"registry:ui","title":"infinite-slider","description":"A InfiniteSlider component for SmoothUI.","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion","react-use-measure"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport { cn } from \"@repo/shadcn-ui/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        ease: \"linear\",\n        duration: transitionDuration,\n        onComplete: () => {\n          setIsTransitioning(false);\n          setKey((prevKey) => prevKey + 1);\n        },\n      });\n    } else {\n      controls = animate(translation, [from, to], {\n        ease: \"linear\",\n        duration,\n        repeat: Number.POSITIVE_INFINITY,\n        repeatType: \"loop\",\n        repeatDelay: 0,\n        onRepeat: () => {\n          translation.set(from);\n        },\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        onHoverStart: () => {\n          setIsTransitioning(true);\n          setCurrentSpeed(speedOnHover);\n        },\n        onHoverEnd: () => {\n          setIsTransitioning(true);\n          setCurrentSpeed(speed);\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          gap: `${gap}px`,\n          flexDirection: direction === \"horizontal\" ? \"row\" : \"column\",\n        }}\n        {...hoverProps}\n      >\n        {children}\n        {children}\n      </motion.div>\n    </div>\n  );\n}\n","target":"components/smoothui/infinite-slider/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"interactive-image-selector","type":"registry:ui","title":"interactive-image-selector","description":"A InteractiveImageSelector component for SmoothUI.","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion","lucide-react"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","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                  scale: [RESET_SCALE_START, RESET_SCALE_PEAK, RESET_SCALE_END],\n                  rotate: [\n                    RESET_ROTATE_START,\n                    RESET_ROTATE_POSITIVE,\n                    RESET_ROTATE_NEGATIVE,\n                    RESET_ROTATE_START,\n                  ],\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                  scale: [\n                    SELECT_SCALE_START,\n                    SELECT_SCALE_PEAK,\n                    SELECT_SCALE_END,\n                  ],\n                  rotate: [\n                    SELECT_ROTATE_START,\n                    SELECT_ROTATE_NEGATIVE,\n                    SELECT_ROTATE_POSITIVE,\n                    SELECT_ROTATE_START,\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                scale: isResetting\n                  ? [ITEM_SCALE_START, ITEM_SCALE_MIN, ITEM_SCALE_END]\n                  : 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              }}\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                type: \"spring\" as const,\n                stiffness: 300,\n                damping: 25,\n                duration: isResetting ? RESET_ANIMATION_DURATION : undefined,\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                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        )}\n      </AnimatePresence>\n    </div>\n  );\n}\n","target":"components/smoothui/interactive-image-selector/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"job-listing-component","type":"registry:ui","title":"job-listing-component","description":"A JobListingComponent component for SmoothUI.","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion","usehooks-ts"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","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                      type: \"spring\" as const,\n                      duration: 0.25,\n                      bounce: 0.1,\n                      layout: {\n                        duration: 0.25,\n                        ease: [0.645, 0.045, 0.355, 1],\n                      },\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                    willChange: shouldReduceMotion ? \"auto\" : \"transform\",\n                    flexShrink: 0,\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                        duration: 0.2,\n                        ease: [0.215, 0.61, 0.355, 1],\n                        delay: 0.05,\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                      type: \"spring\" as const,\n                      duration: 0.25,\n                      bounce: 0.1,\n                      layout: {\n                        duration: 0.25,\n                        ease: [0.645, 0.045, 0.355, 1],\n                      },\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                  willChange: shouldReduceMotion ? \"auto\" : \"transform\",\n                  flexShrink: 0,\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                  position: \"absolute\",\n                  top: \"100%\",\n                  opacity: 0,\n                  pointerEvents: \"none\",\n                }}\n              >\n                {role.job_description}\n              </div>\n            </motion.div>\n          ))}\n        </div>\n      </div>\n    </>\n  );\n}\n","target":"components/smoothui/job-listing-component/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"magnetic-button","type":"registry:ui","title":"magnetic-button","description":"Button that subtly follows the cursor with magnetic effect","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["@radix-ui/react-slot","class-variance-authority","motion"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cn } from \"@repo/shadcn-ui/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 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    variants: {\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        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        ghost: \"hover:bg-accent hover:text-accent-foreground\",\n        link: \"text-primary underline-offset-4 hover:underline\",\n      },\n      size: {\n        default: \"h-10 px-4 py-2\",\n        sm: \"h-9 rounded-md px-3\",\n        lg: \"h-11 rounded-md px-8\",\n        icon: \"h-10 w-10\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\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 = { duration: 0.4, bounce: 0.1 },\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    duration: springConfig.duration ?? 0.4,\n    bounce: springConfig.bounce ?? 0.1,\n  });\n  const y = useSpring(0, {\n    duration: springConfig.duration ?? 0.4,\n    bounce: springConfig.bounce ?? 0.1,\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        padding: `${radius / 2}px`,\n        margin: `-${radius / 2}px`,\n      }}\n    >\n      <motion.div style={{ x, y }}>\n        <Comp\n          className={cn(magneticButtonVariants({ variant, size, className }))}\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","target":"components/smoothui/magnetic-button/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"notification-badge","type":"registry:ui","title":"notification-badge","description":"Animated notification badge with count and status variants","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport { cn } from \"@repo/shadcn-ui/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  online: \"bg-emerald-500\",\n  offline: \"bg-gray-400\",\n  busy: \"bg-red-500\",\n  away: \"bg-amber-500\",\n};\n\nconst positionClasses = {\n  \"top-right\": \"-top-1 -right-1\",\n  \"top-left\": \"-top-1 -left-1\",\n  \"bottom-right\": \"-bottom-1 -right-1\",\n  \"bottom-left\": \"-bottom-1 -left-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={{ y: 0, opacity: 1 }}\n          className=\"inline-block\"\n          exit={{ y: direction * -12, opacity: 0 }}\n          initial={{ y: direction * 12, opacity: 0 }}\n          key={value}\n          transition={{ type: \"spring\" as const, duration: 0.3, bounce: 0.1 }}\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              : { type: \"spring\" as const, duration: 0.25, bounce: 0.2 }\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      )}\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","target":"components/smoothui/notification-badge/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"number-flow","type":"registry:ui","title":"number-flow","description":"A NumberFlow component for SmoothUI.","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["lucide-react"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport { cn } from \"@repo/shadcn-ui/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 min-h-screen 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","target":"components/smoothui/number-flow/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"pagination","type":"registry:ui","title":"pagination","description":"Animated pagination with spring-based active page indicator","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion","lucide-react"],"devDependencies":[],"registryDependencies":["https://smoothui.dev/r/smooth-button.json","https://smoothui.dev/r/smooth-button.json"],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport { cn } from \"@repo/shadcn-ui/lib/utils\";\nimport { ChevronLeft, ChevronRight } from \"lucide-react\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { useCallback, useId, useMemo } from \"react\";\nimport { SPRING_DEFAULT } from \"../../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  type: \"spring\" as const,\n  duration: 0.25,\n  bounce: 0.05,\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","target":"components/smoothui/pagination/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"phototab","type":"registry:ui","title":"phototab","description":"A Phototab component for SmoothUI.","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion","@radix-ui/react-tabs"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","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        left: triggerRect.left - listRect.left,\n        top: triggerRect.top - listRect.top,\n        width: triggerRect.width,\n        height: triggerRect.height,\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                opacity: 1,\n                left: bgStyle.left,\n                top: bgStyle.top,\n                width: bgStyle.width,\n                height: bgStyle.height,\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                      type: \"spring\" as const,\n                      stiffness: 400,\n                      damping: 40,\n                      duration: 0.25,\n                    }\n              }\n            />\n          )}\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            height={height}\n            loading=\"lazy\"\n            src={tab.image}\n            width={400}\n          />\n        </TabsContent>\n      ))}\n    </TabsRoot>\n  );\n}\n","target":"components/smoothui/phototab/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"power-off-slide","type":"registry:ui","title":"power-off-slide","description":"A PowerOffSlide component for SmoothUI.","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion","lucide-react"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","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                  : { type: \"spring\" as const, duration: 0.25 }\n              }\n            >\n              <Power className=\"text-red-600\" size={32} />\n            </motion.div>\n          </div>\n        )}\n      </div>\n    </div>\n  );\n}\n","target":"components/smoothui/power-off-slide/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"price-flow","type":"registry:ui","title":"price-flow","description":"A PriceFlow component for SmoothUI.","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":[],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport { cn } from \"@repo/shadcn-ui/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","target":"components/smoothui/price-flow/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"product-card","type":"registry:ui","title":"product-card","description":"Animated product card component for ecommerce","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion"],"devDependencies":[],"registryDependencies":["https://smoothui.dev/r/smooth-button.json"],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport { cn } from \"@repo/shadcn-ui/lib/utils\";\nimport SmoothButton from \"@repo/smoothui/components/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  type: \"spring\" as const,\n  duration: 0.25,\n  bounce: 0.1,\n};\n\nconst SPRING_BOUNCY = {\n  type: \"spring\" as const,\n  duration: 0.3,\n  bounce: 0.2,\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={{ once: true, margin: \"-50px\" }}\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          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        )}\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 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          )}\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","target":"components/smoothui/product-card/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"radio-group","type":"registry:ui","title":"radio-group","description":"An animated Radio Group component for SmoothUI with selection indicator spring animation.","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion","radix-ui"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport { cn } from \"@repo/shadcn-ui/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 \"../../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  type: \"spring\" as const,\n  duration: 0.25,\n  bounce: 0.2,\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      {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      )}\n    </>\n  );\n}\n\nRadio.displayName = \"Radio\";\n\nexport default RadioGroup;\n","target":"components/smoothui/radio-group/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"reveal-text","type":"registry:ui","title":"reveal-text","description":"A RevealText component for SmoothUI.","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","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  up: { y: 24, opacity: 0 },\n  down: { y: -24, opacity: 0 },\n  left: { x: 24, opacity: 0 },\n  right: { x: -24, opacity: 0 },\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          : { x: 0, y: 0, opacity: 1 }\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              duration: REVEAL_ANIMATION_DURATION_S,\n              delay: delay / MILLISECONDS_TO_SECONDS,\n            }\n      }\n    >\n      {children}\n    </motion.span>\n  );\n};\n\nexport default RevealText;\n","target":"components/smoothui/reveal-text/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"reviews-carousel","type":"registry:ui","title":"reviews-carousel","description":"A ReviewsCarousel component for SmoothUI.","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion","lucide-react"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport { cn } from \"@repo/shadcn-ui/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        y,\n        scale,\n        transition: {\n          type: \"spring\" as const,\n          stiffness: 250,\n          damping: 20,\n          mass: 0.5,\n          duration: 0.25,\n        },\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        willChange: \"opacity, filter, transform\",\n        filter: `blur(${blur}px)`,\n        opacity,\n        transitionProperty: \"opacity, filter\",\n        transitionDuration: shouldReduceMotion ? \"0ms\" : \"250ms\",\n        transitionTimingFunction: \"cubic-bezier(0.4, 0, 0.2, 1)\",\n        zIndex: totalCards - index,\n        pointerEvents: isActive ? \"auto\" : \"none\",\n        top: \"50%\", // Centrar verticalmente\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          )}\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          )}\n          {showNavigation && (\n            <NavigationButton\n              direction=\"next\"\n              disabled={activeIndex === maxIndex}\n              onClick={goToNext}\n            />\n          )}\n        </div>\n      )}\n    </div>\n  );\n}\n","target":"components/smoothui/reviews-carousel/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"rich-popover","type":"registry:ui","title":"rich-popover","description":"A RichPopover component for SmoothUI.","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion","lucide-react","@radix-ui/react-popover"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","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                : { opacity: 1, scale: 1, y: 0, filter: \"blur(0px)\" }\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                : { opacity: 0, scale: 0.95, y: 5, filter: \"blur(8px)\" }\n            }\n            initial={\n              shouldReduceMotion\n                ? { opacity: 1 }\n                : { opacity: 0, scale: 0.95, y: 5, filter: \"blur(8px)\" }\n            }\n            transition={\n              shouldReduceMotion\n                ? { duration: 0 }\n                : {\n                    type: \"spring\" as const,\n                    stiffness: 500,\n                    damping: 30,\n                    duration: 0.2,\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            )}\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            )}\n\n            {/* Tail */}\n            <PopoverArrow className=\"fill-black\" />\n          </motion.div>\n        </PopoverContent>\n      </PopoverPortal>\n    </PopoverRoot>\n  );\n}\n","target":"components/smoothui/rich-popover/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"scramble-hover","type":"registry:ui","title":"scramble-hover","description":"A ScrambleHover component for SmoothUI.","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":[],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","content":"import 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        cursor: \"pointer\",\n        display: \"inline-block\",\n        background: \"none\",\n        border: \"none\",\n        padding: 0,\n        font: \"inherit\",\n        color: \"inherit\",\n        textAlign: \"inherit\",\n      }}\n      type=\"button\"\n    >\n      {display}\n    </button>\n  );\n};\n\nexport default ScrambleHover;\n","target":"components/smoothui/scramble-hover/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"scroll-reveal-paragraph","type":"registry:ui","title":"scroll-reveal-paragraph","description":"A ScrollRevealParagraph component for SmoothUI.","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","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    target: container,\n    offset: [\"start 0.9\", \"start 0.25\"],\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","target":"components/smoothui/scroll-reveal-paragraph/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"scrollable-card-stack","type":"registry:ui","title":"scrollable-card-stack","description":"A ScrollableCardStack component for SmoothUI.","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport { cn } from \"@repo/shadcn-ui/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        y,\n        scale,\n        opacity,\n        blur,\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                      y: `calc(-50% + ${transform.y}px)`,\n                      scale: transform.scale,\n                      x: \"-50%\",\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                height: `${cardHeight}px`,\n                zIndex: transform.zIndex,\n                pointerEvents: isActive ? \"auto\" : \"none\",\n                transformOrigin: \"center center\",\n                willChange: shouldReduceMotion\n                  ? undefined\n                  : \"opacity, filter, transform\",\n                filter: `blur(${transform.blur}px)`,\n                opacity: transform.opacity,\n                transitionProperty: shouldReduceMotion\n                  ? \"none\"\n                  : \"opacity, filter\",\n                transitionDuration: shouldReduceMotion ? \"0ms\" : \"200ms\",\n                transitionTimingFunction:\n                  \"cubic-bezier(0.645, 0.045, 0.355, 1)\",\n                // Dynamic border width based on scale - from reference code\n                borderWidth: `${2 / transform.scale}px`,\n              }}\n              tabIndex={isActive ? 0 : -1}\n              transition={\n                shouldReduceMotion\n                  ? { duration: 0 }\n                  : {\n                      type: \"spring\" as const,\n                      stiffness: 250,\n                      damping: 20,\n                      mass: 0.5,\n                      duration: 0.25,\n                    }\n              }\n              whileHover={\n                shouldReduceMotion || !isActive\n                  ? {}\n                  : {\n                      scale: transform.scale * HOVER_SCALE_MULTIPLIER,\n                      transition: {\n                        type: \"spring\" as const,\n                        stiffness: 250,\n                        damping: 20,\n                        mass: 0.5,\n                        duration: 0.25,\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                )}\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                    height={10}\n                    src=\"data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAiIGhlaWdodD0iMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHJlY3Qgd2lkdGg9IjEwIiBoZWlnaHQ9IjEwIiBmaWxsPSIjZjNmNGY2Ii8+PC9zdmc+\"\n                    style={{\n                      filter: \"blur(32px)\",\n                      scale: \"1.2\",\n                      zIndex: 1,\n                      pointerEvents: \"none\",\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                      zIndex: 2,\n                      pointerEvents: \"none\",\n                      userSelect: \"none\",\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                    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 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                type: \"spring\" as const,\n                stiffness: 250,\n                damping: 20,\n                mass: 0.5,\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","target":"components/smoothui/scrollable-card-stack/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"scrubber","type":"registry:ui","title":"scrubber","description":"Design-tool style scrubber slider with animated thumb and built-in label","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport { cn } from \"@repo/shadcn-ui/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    : { type: \"spring\" as const, duration: 0.25, bounce: 0.1 };\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          height: 52,\n          borderRadius: 12,\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: 12,\n            width: `${percentage}%`,\n            transition: isDragging\n              ? \"none\"\n              : \"width 150ms cubic-bezier(0.23, 1, 0.32, 1)\",\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                    left: `${pos}%`,\n                    width: 1,\n                    height: 8,\n                    borderRadius: 999,\n                    transform: \"translateX(-50%) translateY(-50%)\",\n                  }}\n                />\n              );\n            })}\n          </div>\n        )}\n\n        {/* Scrub bar (capsule thumb) */}\n        <div\n          className=\"pointer-events-none absolute\"\n          style={{\n            top: \"50%\",\n            left: `${percentage}%`,\n            transform: \"translateX(-50%) translateY(-50%)\",\n            marginLeft: -6,\n            zIndex: 3,\n            transition: isDragging\n              ? \"none\"\n              : \"left 150ms cubic-bezier(0.23, 1, 0.32, 1)\",\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              width: 5,\n              height: 34,\n              borderRadius: 999,\n            }}\n            transition={springConfig}\n          />\n        </div>\n\n        {/* Label */}\n        <div\n          className=\"pointer-events-none absolute top-1/2 left-[18px] -translate-y-1/2 whitespace-nowrap text-foreground\"\n          style={{\n            fontSize: 17,\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-[14px] -translate-y-1/2 text-foreground\"\n          style={{\n            zIndex: 4,\n            fontFamily: \"ui-monospace, monospace\",\n            fontVariantNumeric: \"tabular-nums\",\n            fontSize: 15,\n            fontWeight: 500,\n          }}\n        >\n          {value.toFixed(decimals)}\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default Scrubber;\n","target":"components/smoothui/scrubber/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"searchable-dropdown","type":"registry:ui","title":"searchable-dropdown","description":"A SearchableDropdown component for SmoothUI.","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion","lucide-react"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","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({ top: 0, left: 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 label = item.label.toLowerCase();\n      const description = item.description?.toLowerCase();\n\n      if (label.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        top: rect.bottom + 4,\n        left: rect.left,\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          top: rect.bottom + 4,\n          left: rect.left,\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, y: 0, scaleY: 1 }\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                    y: -10,\n                    scaleY: 0.8,\n                    transition: { duration: 0.15 },\n                  }\n            }\n            initial={\n              shouldReduceMotion\n                ? { opacity: 1 }\n                : { opacity: 0, y: -10, scaleY: 0.8 }\n            }\n            style={{\n              top: `${position.top}px`,\n              left: `${position.left}px`,\n              width: `${position.width}px`,\n            }}\n            transition={\n              shouldReduceMotion\n                ? { duration: 0 }\n                : {\n                    type: \"spring\" as const,\n                    stiffness: 400,\n                    damping: 30,\n                    mass: 0.8,\n                    duration: 0.25,\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                        type: \"spring\" as const,\n                        stiffness: 400,\n                        damping: 25,\n                        delay: 0.05,\n                        duration: 0.2,\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-primary 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-primary focus-visible:ring-offset-2\"\n                      exit={{ opacity: 0 }}\n                      initial={{ opacity: 0 }}\n                      onClick={handleClearSearch}\n                      transition={{\n                        type: \"spring\" as const,\n                        stiffness: 400,\n                        damping: 25,\n                      }}\n                      type=\"button\"\n                    >\n                      <X aria-hidden=\"true\" className=\"h-4 w-4\" />\n                    </motion.button>\n                  )}\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                          : { opacity: 1, x: 0, filter: \"blur(0px)\" }\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, filter: \"blur(4px)\" }\n                      }\n                      initial={\n                        shouldReduceMotion\n                          ? { opacity: 1 }\n                          : { opacity: 0, x: -10, filter: \"blur(4px)\" }\n                      }\n                      key={item.id}\n                      layout\n                      role=\"option\"\n                      transition={\n                        shouldReduceMotion\n                          ? { duration: 0 }\n                          : {\n                              type: \"spring\" as const,\n                              stiffness: 400,\n                              damping: 28,\n                              mass: 0.6,\n                              delay: index * 0.02,\n                              duration: 0.2,\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-primary 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                        )}\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                          )}\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                                    type: \"spring\" as const,\n                                    stiffness: 400,\n                                    damping: 25,\n                                    mass: 0.5,\n                                    duration: 0.2,\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                            type: \"spring\" as const,\n                            stiffness: 400,\n                            damping: 25,\n                            duration: 0.2,\n                          }\n                    }\n                  >\n                    {emptyMessage}\n                  </motion.li>\n                )}\n              </AnimatePresence>\n            </ul>\n          </motion.div>\n        </div>\n      )}\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 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-primary 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            {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                    type: \"spring\" as const,\n                    stiffness: 400,\n                    damping: 25,\n                    duration: 0.2,\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","target":"components/smoothui/searchable-dropdown/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"select","type":"registry:ui","title":"select","description":"An animated Select dropdown component for SmoothUI wrapping Radix Select with smooth animations.","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["lucide-react","motion"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport { cn } from \"@repo/shadcn-ui/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 \"../../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({ top: 0, left: 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        top: rect.bottom + DROPDOWN_OFFSET,\n        left: rect.left,\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          top: rect.bottom + DROPDOWN_OFFSET,\n          left: rect.left,\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, globalIndex: number) => {\n    const isSelected = opt.value === selectedValue;\n    const isFocused = globalIndex === 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: globalIndex * 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(globalIndex)}\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 ? {} : { scale: 1, opacity: 1 }}\n                  exit={\n                    shouldReduceMotion\n                      ? { opacity: 0, transition: { duration: 0 } }\n                      : { scale: 0, opacity: 0 }\n                  }\n                  initial={shouldReduceMotion ? {} : { scale: 0, opacity: 0 }}\n                  transition={\n                    shouldReduceMotion\n                      ? DURATION_INSTANT\n                      : {\n                          type: \"spring\" as const,\n                          stiffness: 300,\n                          damping: 20,\n                          duration: 0.2,\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                    y: -4,\n                    transition: { duration: 0.15 },\n                  }\n            }\n            initial={\n              shouldReduceMotion\n                ? { opacity: 1 }\n                : { opacity: 0, scale: 0.95, y: -4 }\n            }\n            role=\"listbox\"\n            style={{\n              top: `${position.top}px`,\n              left: `${position.left}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            </div>\n          </motion.div>\n        </div>\n      )}\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        )}\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 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                : { type: \"spring\" as const, duration: 0.25, bounce: 0.05 }\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","target":"components/smoothui/select/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"siri-orb","type":"registry:ui","title":"siri-orb","description":"A beautiful animated orb component inspired by Siri's visual design.","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":[],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport { cn } from \"@repo/shadcn-ui/lib/utils\";\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\nexport interface SiriOrbProps {\n  animationDuration?: number;\n  className?: string;\n  colors?: {\n    bg?: string;\n    c1?: string;\n    c2?: string;\n    c3?: string;\n  };\n  size?: string;\n}\n\nconst SiriOrb: React.FC<SiriOrbProps> = ({\n  size = \"192px\",\n  className,\n  colors,\n  animationDuration = 20,\n}) => {\n  const defaultColors = {\n    bg: \"oklch(95% 0.02 264.695)\",\n    c1: \"oklch(75% 0.15 350)\", // Pastel pink\n    c2: \"oklch(80% 0.12 200)\", // Pastel blue\n    c3: \"oklch(78% 0.14 280)\", // Pastel purple/lavender\n  };\n\n  const finalColors = { ...defaultColors, ...colors };\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  return (\n    <div\n      className={cn(\"siri-orb\", className)}\n      style={\n        {\n          width: size,\n          height: size,\n          \"--bg\": finalColors.bg,\n          \"--c1\": finalColors.c1,\n          \"--c2\": finalColors.c2,\n          \"--c3\": finalColors.c3,\n          \"--animation-duration\": `${animationDuration}s`,\n          \"--blur-amount\": `${blurAmount}px`,\n          \"--contrast-amount\": finalContrast,\n          \"--dot-size\": `${dotSize}px`,\n          \"--shadow-spread\": `${shadowSpread}px`,\n          \"--mask-radius\": maskRadius,\n        } as React.CSSProperties\n      }\n    >\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        }\n\n        .siri-orb::before,\n        .siri-orb::after {\n          content: \"\";\n          display: block;\n          grid-area: stack;\n          width: 100%;\n          height: 100%;\n          border-radius: 50%;\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) * 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          filter: blur(var(--blur-amount)) contrast(var(--contrast-amount));\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            animation: none;\n          }\n        }\n      `}</style>\n    </div>\n  );\n};\n\nexport default SiriOrb;\n","target":"components/smoothui/siri-orb/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"skeleton-loader","type":"registry:ui","title":"skeleton-loader","description":"Animated skeleton loading placeholders","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":[],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport { cn } from \"@repo/shadcn-ui/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","target":"components/smoothui/skeleton-loader/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"smooth-button","type":"registry:ui","title":"smooth-button","description":"A polished button component with gradient variants and press animation","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["@radix-ui/react-slot","class-variance-authority"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cn } from \"@repo/shadcn-ui/lib/utils\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport type React from \"react\";\nimport type { ButtonHTMLAttributes } from \"react\";\n\nconst smoothButtonVariants = 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    variants: {\n      variant: {\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        outline:\n          \"border border-input bg-background shadow-xs hover:bg-accent hover:text-white dark:bg-input/30 dark:hover:bg-input/50\",\n        secondary:\n          \"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80\",\n        ghost: \"hover:bg-background hover:text-foreground hover:shadow-custom\",\n        link: \"text-primary underline-offset-4 hover:underline\",\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      },\n      size: {\n        default: \"h-10 px-4 py-2\",\n        sm: \"h-9 rounded-md px-4 py-2\",\n        lg: \"h-11 rounded-md px-8\",\n        icon: \"h-10 w-10\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  }\n);\n\nexport type SmoothButtonProps = ButtonHTMLAttributes<HTMLButtonElement> &\n  VariantProps<typeof smoothButtonVariants> & {\n    asChild?: boolean;\n    ref?: React.Ref<HTMLButtonElement>;\n  };\n\nfunction SmoothButton({\n  className,\n  variant,\n  size,\n  asChild = false,\n  ref,\n  ...props\n}: SmoothButtonProps) {\n  const Comp = asChild ? Slot : \"button\";\n  return (\n    <Comp\n      className={cn(smoothButtonVariants({ variant, size, className }))}\n      ref={ref}\n      {...props}\n    />\n  );\n}\n\nexport default SmoothButton;\nexport { smoothButtonVariants };\n","target":"components/smoothui/smooth-button/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"social-selector","type":"registry:ui","title":"social-selector","description":"A SocialSelector component for SmoothUI.","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","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    name: \"X\",\n    domain: \"x.com\",\n    icon: <XIcon className=\"h-5 w-5\" />,\n    url: \"https://x.com/educalvolpz\",\n  },\n  {\n    name: \"Bluesky\",\n    domain: \"bsky.app\",\n    icon: <BskyIcon className=\"h-5 w-5\" />,\n    url: \"https://bsky.app/profile/educalvolpz.bsky.social\",\n  },\n  {\n    name: \"Threads\",\n    domain: \"threads.net\",\n    icon: <ThreadsIcon className=\"h-5 w-5\" />,\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                      type: \"spring\" as const,\n                      stiffness: 500,\n                      damping: 30,\n                      duration: 0.25,\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                  : { opacity: 1, y: 0, filter: \"blur(0px)\" }\n              }\n              exit={\n                shouldReduceMotion\n                  ? { opacity: 0, transition: { duration: 0 } }\n                  : { opacity: 0, y: -10, filter: \"blur(5px)\" }\n              }\n              href={selectedPlatform.url}\n              initial={\n                shouldReduceMotion\n                  ? { opacity: 1 }\n                  : { opacity: 0, y: 10, filter: \"blur(5px)\" }\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","target":"components/smoothui/social-selector/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"switchboard-card","type":"registry:ui","title":"switchboard-card","description":"A SwitchboardCard component with light grid illustration for SmoothUI.","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":[],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport { cn } from \"@repo/shadcn-ui/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            gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))`,\n            gridTemplateRows: `repeat(${rows}, minmax(0, 1fr))`,\n            gap: `min(1px, calc(100% / ${columns} / 10))`,\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=\"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            opacity: 1,\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          }}\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            opacity: 1,\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          }}\n        />\n      )}\n    </div>\n  );\n}\n","target":"components/smoothui/switchboard-card/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"tweet-card","type":"registry:ui","title":"tweet-card","description":"A beautiful tweet card component for displaying Twitter/X posts.","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["react-tweet"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"client.tsx","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","target":"components/smoothui/tweet-card/client.tsx"},{"type":"registry:ui","path":"index.tsx","content":"/* eslint-disable @next/next/no-img-element */\n\nimport { cn } from \"@repo/shadcn-ui/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-primary/10\", 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        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      )}\n      {hasPhotos &&\n        tweet.photos &&\n        (() => {\n          const photos = tweet.photos;\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                  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                      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                      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      {!(hasVideo || hasPhotos) && hasCardThumbnail && (\n        <img\n          alt={tweet.text}\n          className=\"w-full rounded-xl border object-cover shadow-sm\"\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","target":"components/smoothui/tweet-card/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"typewriter-text","type":"registry:ui","title":"typewriter-text","description":"A TypewriterText component for SmoothUI.","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":[],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","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","target":"components/smoothui/typewriter-text/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"user-account-avatar","type":"registry:ui","title":"user-account-avatar","description":"A UserAccountAvatar component for SmoothUI.","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion","lucide-react","@radix-ui/react-popover"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","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  { id: \"ORD001\", date: \"2023-03-15\", status: \"delivered\", progress: 100 },\n  { id: \"ORD002\", date: \"2023-03-20\", status: \"shipped\", progress: 66 },\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      name: formData.get(\"name\") as string,\n      email: formData.get(\"email\") 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                          type: \"spring\" as const,\n                          stiffness: 300,\n                          damping: 30,\n                          duration: 0.4,\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            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                : { type: \"spring\" as const, duration: 0.25, bounce: 0 }\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                        ? { opacity: 1, height: \"auto\" }\n                        : {\n                            opacity: 1,\n                            height: \"auto\",\n                            filter: \"blur(0px)\",\n                          }\n                    }\n                    exit={\n                      shouldReduceMotion\n                        ? { opacity: 0, height: 0, transition: { duration: 0 } }\n                        : { opacity: 0, height: 0, filter: \"blur(10px)\" }\n                    }\n                    initial={\n                      shouldReduceMotion\n                        ? { opacity: 0, height: 0 }\n                        : { opacity: 0, height: 0, filter: \"blur(10px)\" }\n                    }\n                    transition={\n                      shouldReduceMotion\n                        ? { duration: 0 }\n                        : { type: \"spring\" as const, duration: 0.25, bounce: 0 }\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                        ? { opacity: 1, height: \"auto\" }\n                        : {\n                            opacity: 1,\n                            height: \"auto\",\n                            filter: \"blur(0px)\",\n                          }\n                    }\n                    exit={\n                      shouldReduceMotion\n                        ? { opacity: 0, height: 0, transition: { duration: 0 } }\n                        : { opacity: 0, height: 0, filter: \"blur(10px)\" }\n                    }\n                    initial={\n                      shouldReduceMotion\n                        ? { opacity: 0, height: 0 }\n                        : { opacity: 0, height: 0, filter: \"blur(10px)\" }\n                    }\n                    transition={\n                      shouldReduceMotion\n                        ? { duration: 0 }\n                        : { type: \"spring\" as const, duration: 0.25, bounce: 0 }\n                    }\n                  >\n                    {renderLastOrders()}\n                  </motion.div>\n                )}\n              </AnimatePresence>\n            </div>\n          </motion.div>\n        </PopoverContent>\n      </PopoverPortal>\n    </PopoverRoot>\n  );\n}\n","target":"components/smoothui/user-account-avatar/index.tsx"}],"css":{}},{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"wave-text","type":"registry:ui","title":"wave-text","description":"A WaveText component for SmoothUI.","author":"Eduardo Calvo <educlopez93@gmail.com>","dependencies":["motion"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","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                  repeat: Number.POSITIVE_INFINITY,\n                  duration,\n                  delay: i * staggerDelay,\n                  ease: [0.37, 0, 0.63, 1],\n                  times: [0, 0.25, 0.5, 0.75, 1],\n                }\n          }\n        >\n          {char === \" \" ? \"\\u00A0\" : char}\n        </motion.span>\n      ))}\n    </span>\n  );\n};\n\nexport default WaveText;\n","target":"components/smoothui/wave-text/index.tsx"}],"css":{}}]}