Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions public/r/accordion.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@
"files": [
{
"path": "registry/sonaui/accordion/accordion.tsx",
"type": "registry:ui"
"content": "\"use client\";\n\nimport {\n createContext,\n useContext,\n useEffect,\n useState,\n useRef,\n type ReactNode,\n} from \"react\";\nimport { motion } from \"motion/react\";\n\nimport { cn } from \"@/lib/utils\";\nimport AnimatedPlusMinusButton from \"./animated-plus-minus-button\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\n// Types\ntype AccordionVariant = \"default\" | \"outlined\" | \"splitted\";\n\ninterface AccordionProps {\n children: ReactNode;\n allowMultiple?: boolean;\n className?: string;\n variant?: AccordionVariant;\n}\n\nconst accordionWrapperVarinats = cva(\n \"flex flex-col overflow-clip rounded-2xl\",\n {\n variants: {\n variant: {\n default: \"overflow-clip rounded-2xl\",\n outlined: \"overflow-clip rounded-2xl\",\n splitted: \"overflow-clip rounded-2xl\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n },\n },\n);\n\nconst accordionItemVariants = cva(\n \"relative overflow-hidden bg-background text-foreground\",\n {\n variants: {\n variant: {\n default: \"border-b border-border\",\n outlined:\n \"border-foreground border-t border-x last:border-b first:rounded-t-2xl last:rounded-b-2xl\",\n splitted: \"rounded-2xl \",\n },\n },\n defaultVariants: {\n variant: \"default\",\n },\n },\n);\n\ninterface AccordionItemProps\n extends\n React.HTMLAttributes<HTMLDivElement>,\n VariantProps<typeof accordionItemVariants> {\n children: ReactNode;\n className?: string;\n style?: React.CSSProperties;\n}\n\ninterface AccordionItemHeaderProps {\n value: string;\n children: ReactNode;\n}\ninterface AccordionItemTriggerProps {\n value: string;\n children: ReactNode;\n}\n\ninterface AccordionItemContentProps {\n children: ReactNode;\n value: string;\n}\n\nconst AccordionContext = createContext<{\n openItems: Set<string>;\n toggleItem: (value: string) => void;\n variant: AccordionVariant;\n} | null>(null);\n\nconst AccordionRoot = ({\n children,\n allowMultiple = false,\n className,\n variant = \"default\",\n}: AccordionProps) => {\n const [openItems, setOpenItems] = useState<Set<string>>(new Set());\n const toggleItem = (value: string) => {\n setOpenItems((prev) => {\n const newOpenItems = new Set(prev);\n if (newOpenItems.has(value)) {\n newOpenItems.delete(value);\n } else {\n if (!allowMultiple) newOpenItems.clear();\n newOpenItems.add(value);\n }\n return newOpenItems;\n });\n };\n\n return (\n <AccordionContext.Provider value={{ openItems, toggleItem, variant }}>\n <div\n role=\"presentation\"\n className={cn(\n accordionWrapperVarinats({ variant }),\n variant === \"splitted\" && \"gap-y-2\",\n className,\n )}\n >\n {children}\n </div>\n </AccordionContext.Provider>\n );\n};\n\nconst AccordionItem = ({\n children,\n className,\n style,\n ...props\n}: AccordionItemProps) => {\n const context = useContext(AccordionContext);\n if (!context)\n throw new Error(\"AccordionItem must be used within AccordionRoot\");\n const { variant } = context;\n return (\n <div\n role=\"presentation\"\n className={cn(accordionItemVariants({ variant }), className)}\n style={style}\n {...props}\n >\n <div className=\"relative\">{children}</div>\n </div>\n );\n};\n\nconst AccordionItemHeader = ({ value, children }: AccordionItemHeaderProps) => {\n const context = useContext(AccordionContext);\n if (!context)\n throw new Error(\"AccordionTrigger must be used within AccordionRoot\");\n const { openItems } = context;\n\n const isOpen = openItems.has(value);\n\n return (\n <div className=\"flex items-center justify-between rounded-xl px-8 pt-4 font-medium text-balance\">\n <div className=\"\">{children}</div>\n <AnimatedPlusMinusButton isOpen={isOpen} />\n </div>\n );\n};\n\nconst AccordionItemTrigger = ({\n value,\n children,\n}: AccordionItemTriggerProps) => {\n const context = useContext(AccordionContext);\n if (!context)\n throw new Error(\"AccordionTrigger must be used within AccordionRoot\");\n\n const { openItems, toggleItem } = context;\n const isOpen = openItems.has(value);\n\n return (\n <div\n aria-expanded={isOpen}\n onClick={() => toggleItem(value)}\n className=\"cursor-pointer\"\n >\n {children}\n </div>\n );\n};\n\nconst AccordionItemContent = ({\n children,\n value,\n}: AccordionItemContentProps) => {\n const context = useContext(AccordionContext);\n if (!context)\n throw new Error(\"AccordionContent must be used within AccordionRoot\");\n\n const { openItems } = context;\n const isOpen = openItems.has(value);\n\n const ref = useRef<HTMLDivElement>(null);\n const [height, setHeight] = useState(0);\n\n useEffect(() => {\n if (ref.current) {\n setHeight(ref.current.offsetHeight + 16);\n }\n }, [isOpen]);\n\n const variants = {\n open: { opacity: [0, 0.5, 1], y: 0 },\n exit: { opacity: 0, y: 50 },\n initial: { opacity: 0, y: 50 },\n };\n\n return (\n <motion.div\n role=\"region\"\n aria-hidden={!isOpen}\n className={`overflow-hidden px-8 py-2 text-sm transition-[height] duration-300`}\n initial={{ height: 0 }}\n animate={{ height: isOpen ? height : 0 }}\n transition={{ duration: 0.3, ease: \"easeIn\" }}\n >\n <motion.div\n initial=\"initial\"\n animate={isOpen ? \"open\" : \"exit\"}\n transition={{\n duration: 0.4,\n ease: \"easeIn\",\n delay: 0.3,\n type: \"tween\",\n }}\n // className=\"pb-2\"\n variants={variants}\n ref={ref}\n >\n {children}\n </motion.div>\n </motion.div>\n );\n};\n\nexport {\n AccordionRoot,\n AccordionItem,\n AccordionItemHeader,\n AccordionItemTrigger,\n AccordionItemContent,\n AccordionContext,\n};\n",
"type": "registry:ui",
"target": "components/sona-ui/accordion/accordion.tsx"
},
{
"path": "registry/sonaui/accordion/animated-plus-minus-button.tsx",
"type": "registry:ui"
"content": "\"use client\";\nimport { motion } from \"framer-motion\";\n\ninterface AnimatedPlusMinusButtonProps {\n size?: number;\n isOpen?: boolean;\n}\nconst AnimatedPlusMinusButton = ({\n size = 24,\n isOpen = false,\n}: AnimatedPlusMinusButtonProps) => {\n return (\n <span\n role=\"button\"\n aria-pressed=\"true\"\n className=\"flex items-center justify-center\"\n >\n <motion.svg\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n initial=\"closed\"\n animate={isOpen ? \"open\" : \"closed\"}\n >\n {/* Horizontal line */}\n <motion.line\n x1=\"1\"\n y1=\"12\"\n x2=\"23\"\n y2=\"12\"\n stroke=\"currentColor\"\n strokeWidth=\"1\"\n variants={{\n closed: { opacity: 1 },\n open: { opacity: 0 },\n }}\n transition={{ duration: 0.3 }}\n />\n\n {/* Vertical line */}\n <motion.line\n x1=\"12\"\n y1=\"1\"\n x2=\"12\"\n y2=\"23\"\n stroke=\"currentColor\"\n strokeWidth=\"1\"\n variants={{\n closed: { rotate: 0, opacity: 1 },\n open: { rotate: 90, opacity: 1 },\n }}\n transition={{ duration: 0.3 }}\n />\n </motion.svg>\n </span>\n );\n};\n\nexport default AnimatedPlusMinusButton;\n",
"type": "registry:ui",
"target": "components/sona-ui/accordion/animated-plus-minus-button.tsx"
}
],
"dependencies": [
Expand Down
4 changes: 3 additions & 1 deletion public/r/bubble-up-button.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
"files": [
{
"path": "registry/sonaui/bubble-up-button/bubble-up-button.tsx",
"type": "registry:ui"
"content": "\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport { motion, MotionConfigProps, useAnimation } from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\n\ninterface BubbleUpButtonProps\n extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n /** Content to display inside the button */\n children?: ReactNode;\n /** Motion configuration for animations */\n motionControls?: MotionConfigProps;\n /** Additional CSS classes */\n className?: string;\n}\nexport default function BubbleUpButton({\n children = \"Hover me!\",\n motionControls = {\n transition: { type: \"spring\", stiffness: 200, damping: 40 },\n },\n className = \"\",\n disabled = false,\n ...props\n}: BubbleUpButtonProps) {\n const controls = useAnimation();\n\n const handleMouseEnter = async () => {\n await controls.start({\n clipPath: \"ellipse(120% 120% at 50% 100%)\",\n });\n };\n\n const handleMouseLeave = async () => {\n await controls.start({\n clipPath: \"ellipse(120% 120% at 50% -120%)\",\n });\n controls.set({ clipPath: \"ellipse(0% 0% at 50% 100%)\" });\n };\n\n return (\n <button\n onMouseEnter={handleMouseEnter}\n onMouseLeave={handleMouseLeave}\n disabled={disabled}\n className={cn(\n \"relative flex h-fit w-fit cursor-pointer overflow-clip rounded-2xl border bg-black px-16 py-2\",\n \"transition-opacity duration-200\",\n \"focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-black focus:outline-none\",\n disabled && \"cursor-not-allowed opacity-50\",\n className,\n )}\n aria-label={typeof children === \"string\" ? children : \"Bubble up button\"}\n {...props}\n >\n <motion.div\n animate={controls}\n initial={{ clipPath: \"ellipse(0% 0% at 50% 100%)\" }}\n transition={motionControls.transition}\n className=\"absolute top-0 left-0 h-full w-full bg-white\"\n aria-hidden=\"true\"\n />\n <span className=\"relative text-white mix-blend-difference\">\n {children}\n </span>\n </button>\n );\n}\n",
"type": "registry:ui",
"target": "components/sona-ui/bubble-up-button/bubble-up-button.tsx"
}
],
"dependencies": [],
Expand Down
4 changes: 3 additions & 1 deletion public/r/expandable-tabs.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
"files": [
{
"path": "registry/sonaui/expandable-tabs/expandable-tabs.tsx",
"type": "registry:ui"
"content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { motion, MotionConfig, type MotionConfigProps } from \"motion/react\";\nimport { IconType } from \"react-icons\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport type TabsData = {\n title: string;\n icon: IconType;\n};\n\ninterface ExpandableTabsProps {\n tabs: TabsData[];\n containerClassName?: string;\n defaultActiveIndex?: number;\n motionConfig?: MotionConfigProps;\n}\n\nexport default function ExpandableTabs({\n tabs,\n containerClassName = \"\",\n defaultActiveIndex = 0,\n motionConfig = {\n transition: { duration: 0.2, ease: \"easeInOut\" },\n },\n}: ExpandableTabsProps) {\n const [isActive, setIsActive] = useState(defaultActiveIndex);\n\n return (\n <motion.div\n className={cn(\n \"flex space-x-2 rounded-full border bg-transparent p-2 transition-[width] will-change-[width,_contents]\",\n containerClassName,\n )}\n layout\n >\n <MotionConfig {...motionConfig}>\n {tabs.map((tab, index) => (\n <motion.div\n key={index}\n role=\"button\"\n className={cn(\n \"flex cursor-pointer items-center space-x-2 overflow-clip rounded-full p-2\",\n \"transition-[width,_background-color] duration-300 ease-in-out\",\n isActive === index && \"bg-slate-300 dark:text-slate-800\",\n )}\n onClick={() => setIsActive(index)}\n tabIndex={0}\n >\n <div className=\"h-full grow items-center justify-center\">\n <tab.icon className=\"text-lg\" aria-label={`${tab.title}-icon`} />\n </div>\n <motion.span\n className=\"overflow-hidden text-sm leading-[1]\"\n key={tab.title}\n initial={isActive === index ? \"\" : \"inactive\"}\n animate={isActive === index ? \"active\" : \"inactive\"}\n variants={TabItemVariants}\n >\n {tab.title}\n </motion.span>\n </motion.div>\n ))}\n </MotionConfig>\n </motion.div>\n );\n}\n\nconst TabItemVariants = {\n active: {\n opacity: 1,\n width: \"auto\",\n y: 0,\n },\n inactive: {\n opacity: 0,\n width: 0,\n y: 20,\n },\n};\n",
"type": "registry:ui",
"target": "components/sona-ui/expandable-tabs/expandable-tabs.tsx"
}
],
"dependencies": [
Expand Down
4 changes: 3 additions & 1 deletion public/r/link-preview.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
"files": [
{
"path": "registry/sonaui/link-preview/link-preview.tsx",
"type": "registry:ui"
"content": "\"use client\";\n\nimport { useState } from \"react\";\nimport Link from \"next/link\";\n\nimport { AnimatePresence, motion } from \"motion/react\";\nimport { FaArrowUpRightFromSquare } from \"react-icons/fa6\";\nimport useMeasure from \"react-use-measure\";\nimport { useMediaQuery } from \"@/hooks/useMediaQuery\";\n\ninterface LinkPreviewProps extends React.HTMLAttributes<HTMLAnchorElement> {\n link: string;\n text: string;\n showIcon?: boolean;\n}\n\nexport default function LinkPreview({\n link,\n text,\n showIcon = true,\n ...linkProps\n}: LinkPreviewProps) {\n const [previewRef, previewBounds] = useMeasure();\n const [containerRef, containerBounds] = useMeasure();\n const desktop = useMediaQuery(\"(min-width: 768px)\");\n\n const [isHover, setIsHover] = useState(false);\n\n return (\n <>\n <Link\n href={link}\n className=\"relative inline-flex cursor-pointer items-center underline underline-offset-3\"\n onMouseEnter={(e) => {\n if (!desktop) return;\n e.preventDefault();\n setIsHover(true);\n }}\n onClick={(e) => {\n if (!desktop) return;\n e.preventDefault();\n setIsHover((prev) => !prev);\n }}\n onMouseLeave={() => {\n if (isHover) {\n setIsHover(false);\n }\n }}\n onFocus={(e) => {\n e.preventDefault();\n setIsHover(true);\n }}\n onBlur={() => setIsHover(false)}\n ref={containerRef}\n >\n {text}\n {showIcon && (\n <span className=\"ml-1 text-sm\">\n <FaArrowUpRightFromSquare />\n </span>\n )}\n </Link>\n <AnimatePresence>\n {isHover && desktop && (\n <motion.div\n ref={previewRef}\n className=\"absolute z-50 w-fit origin-center overflow-clip rounded-xl border border-slate-400 bg-slate-100 shadow-xl dark:bg-slate-600\"\n style={{\n left: containerBounds.left - previewBounds.width / 2,\n top: containerBounds.top - previewBounds.height,\n }}\n initial={{ opacity: 0, width: 0, height: 0 }}\n animate={{ opacity: 1, width: \"fit-content\", height: \"auto\" }}\n exit={{ opacity: 0 }}\n transition={{ duration: 0.2 }}\n onMouseEnter={() => setIsHover(true)}\n onMouseLeave={() => setIsHover(false)}\n >\n <motion.div className=\"flex w-fit flex-col gap-y-2 rounded-xl px-4 py-2\">\n <div className=\"flex w-full justify-between text-sm\">\n External Link\n <Link href={link}>\n <FaArrowUpRightFromSquare />\n </Link>\n </div>\n <Link\n href={link}\n className=\"text-nowrap underline\"\n {...linkProps}\n >\n {link}\n </Link>\n </motion.div>\n </motion.div>\n )}\n </AnimatePresence>\n </>\n );\n}\n",
"type": "registry:ui",
"target": "components/sona-ui/link-preview/link-preview.tsx"
}
],
"dependencies": [
Expand Down
Loading