{
  "product": "SaaS Design — Free Starter",
  "framework": "react",
  "available_frameworks": [
    "react",
    "vue",
    "html"
  ],
  "license": "Free tier. Personal use. Upgrade for commercial rights + the full system: https://www.saasdesign.io/pricing/",
  "docs": "https://www.saasdesign.io/components/",
  "requires": [
    "react",
    "tailwindcss@^4",
    "lucide-react"
  ],
  "instructions": "Add globals.css to your stylesheet entry, install ALL deps in `requires` (including lucide-react, which the components import for icons), then drop the files under your components dir. Import like: import { Button } from '@/components/ui/button'.",
  "files": [
    {
      "name": "Tokens",
      "filename": "globals.css",
      "source": "/* =========================================================================\n   SaaS Design: FREE Starter Tokens\n   =========================================================================\n   A clean, standard token set so the 12 free starter components render great\n   out of the box. The premium, hand-tuned token system (richer OKLCH palette,\n   layered elevation, and motion) ships with a paid license. Drop this into your\n   globals.css and the starter components just work, light and dark.\n   ========================================================================= */\n\n@import \"tailwindcss\";\n\n@theme inline {\n  --font-sans: \"Inter\", ui-sans-serif, system-ui, -apple-system, sans-serif;\n\n  --color-background: var(--background);\n  --color-foreground: var(--foreground);\n  --color-card: var(--card);\n  --color-card-foreground: var(--card-foreground);\n  --color-popover: var(--popover);\n  --color-popover-foreground: var(--popover-foreground);\n  --color-primary: var(--primary);\n  --color-primary-foreground: var(--primary-foreground);\n  --color-secondary: var(--secondary);\n  --color-secondary-foreground: var(--secondary-foreground);\n  --color-muted: var(--muted);\n  --color-muted-foreground: var(--muted-foreground);\n  --color-accent: var(--accent);\n  --color-accent-foreground: var(--accent-foreground);\n  --color-destructive: var(--destructive);\n  --color-destructive-foreground: var(--destructive-foreground);\n  --color-success: var(--success);\n  --color-success-foreground: var(--success-foreground);\n  --color-border: var(--border);\n  --color-input: var(--input);\n  --color-ring: var(--ring);\n\n  --radius-sm: calc(var(--radius) - 4px);\n  --radius-md: calc(var(--radius) - 2px);\n  --radius-lg: var(--radius);\n  --radius-xl: calc(var(--radius) + 4px);\n}\n\n:root {\n  --radius: 0.625rem;\n\n  --background: #ffffff;\n  --foreground: #18181b;\n  --card: #ffffff;\n  --card-foreground: #18181b;\n  --popover: #ffffff;\n  --popover-foreground: #18181b;\n  --primary: #2563eb;\n  --primary-foreground: #ffffff;\n  --secondary: #f4f4f5;\n  --secondary-foreground: #27272a;\n  --muted: #f4f4f5;\n  --muted-foreground: #71717a;\n  --accent: #eff6ff;\n  --accent-foreground: #1d4ed8;\n  --destructive: #dc2626;\n  --destructive-foreground: #ffffff;\n  --success: #16a34a;\n  --success-foreground: #ffffff;\n  --border: #e4e4e7;\n  --input: #e4e4e7;\n  --ring: #2563eb;\n}\n\n.dark {\n  --background: #18181b;\n  --foreground: #fafafa;\n  --card: #1f1f23;\n  --card-foreground: #fafafa;\n  --popover: #1f1f23;\n  --popover-foreground: #fafafa;\n  --primary: #3b82f6;\n  --primary-foreground: #ffffff;\n  --secondary: #27272a;\n  --secondary-foreground: #fafafa;\n  --muted: #27272a;\n  --muted-foreground: #a1a1aa;\n  --accent: #1e293b;\n  --accent-foreground: #bfdbfe;\n  --destructive: #ef4444;\n  --destructive-foreground: #ffffff;\n  --success: #22c55e;\n  --success-foreground: #052e16;\n  --border: #32323a;\n  --input: #32323a;\n  --ring: #3b82f6;\n}\n\n@layer base {\n  * {\n    border-color: var(--border);\n  }\n  body {\n    background-color: var(--background);\n    color: var(--foreground);\n    font-family: var(--font-sans);\n    -webkit-font-smoothing: antialiased;\n  }\n}\n"
    },
    {
      "name": "cn helper",
      "filename": "components/ui/cn.ts",
      "source": "// Tiny className joiner that keeps components dependency-free. Filters out falsy\n// values so you can write `cn(\"base\", cond && \"extra\")`.\nexport type ClassValue = string | false | null | undefined;\n\nexport function cn(...parts: ClassValue[]): string {\n  return parts.filter(Boolean).join(\" \");\n}\n"
    },
    {
      "name": "Button",
      "filename": "components/ui/button.tsx",
      "source": "import { forwardRef, type ButtonHTMLAttributes } from \"react\";\nimport { Loader2 } from \"lucide-react\";\nimport { cn } from \"./cn\";\n\nexport type ButtonVariant = \"primary\" | \"secondary\" | \"outline\" | \"ghost\" | \"destructive\" | \"link\";\nexport type ButtonSize = \"sm\" | \"md\" | \"lg\" | \"icon\";\n\nconst base =\n  \"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50\";\n\nconst variants: Record<ButtonVariant, string> = {\n  primary: \"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90\",\n  secondary: \"bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n  outline: \"border border-border bg-background hover:bg-accent hover:text-accent-foreground\",\n  ghost: \"hover:bg-accent hover:text-accent-foreground\",\n  destructive: \"bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90\",\n  link: \"text-primary underline-offset-4 hover:underline\",\n};\n\nconst sizes: Record<ButtonSize, string> = {\n  sm: \"h-8 rounded-md px-3 text-xs\",\n  md: \"h-10 px-4 text-sm\",\n  lg: \"h-11 rounded-md px-6 text-base\",\n  icon: \"h-10 w-10\",\n};\n\nexport interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {\n  variant?: ButtonVariant;\n  size?: ButtonSize;\n  loading?: boolean;\n}\n\nexport const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(\n  { className, variant = \"primary\", size = \"md\", loading = false, children, disabled, ...props },\n  ref,\n) {\n  return (\n    <button\n      ref={ref}\n      className={cn(base, variants[variant], sizes[size], className)}\n      disabled={disabled || loading}\n      {...props}\n    >\n      {loading && <Loader2 className=\"h-4 w-4 animate-spin\" />}\n      {children}\n    </button>\n  );\n});\n"
    },
    {
      "name": "Badge",
      "filename": "components/ui/badge.tsx",
      "source": "import type { HTMLAttributes } from \"react\";\nimport { cn } from \"./cn\";\n\nexport type BadgeVariant =\n  | \"default\"\n  | \"secondary\"\n  | \"outline\"\n  | \"success\"\n  | \"warning\"\n  | \"destructive\"\n  | \"info\";\n\nconst variants: Record<BadgeVariant, string> = {\n  default: \"border-transparent bg-primary/10 text-primary\",\n  secondary: \"border-transparent bg-secondary text-secondary-foreground\",\n  outline: \"border-border text-foreground\",\n  success: \"border-transparent bg-success/12 text-success\",\n  warning: \"border-transparent bg-amber-500/15 text-amber-600 dark:text-amber-400\",\n  destructive: \"border-transparent bg-destructive/12 text-destructive\",\n  info: \"border-transparent bg-primary/10 text-primary\",\n};\n\nexport interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {\n  variant?: BadgeVariant;\n  dot?: boolean;\n}\n\nconst dotColor: Record<BadgeVariant, string> = {\n  default: \"bg-primary\",\n  secondary: \"bg-muted-foreground\",\n  outline: \"bg-muted-foreground\",\n  success: \"bg-success\",\n  warning: \"bg-amber-500\",\n  destructive: \"bg-destructive\",\n  info: \"bg-primary\",\n};\n\nexport function Badge({ className, variant = \"default\", dot = false, children, ...props }: BadgeProps) {\n  return (\n    <span\n      className={cn(\n        \"inline-flex items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-xs font-medium\",\n        variants[variant],\n        className,\n      )}\n      {...props}\n    >\n      {dot && <span className={cn(\"h-1.5 w-1.5 rounded-full\", dotColor[variant])} />}\n      {children}\n    </span>\n  );\n}\n"
    },
    {
      "name": "Card",
      "filename": "components/ui/card.tsx",
      "source": "import type { HTMLAttributes } from \"react\";\nimport { cn } from \"./cn\";\n\nexport function Card({ className, ...props }: HTMLAttributes<HTMLDivElement>) {\n  return (\n    <div\n      className={cn(\"rounded-xl border border-border bg-card text-card-foreground shadow-sm\", className)}\n      {...props}\n    />\n  );\n}\n\nexport function CardHeader({ className, ...props }: HTMLAttributes<HTMLDivElement>) {\n  return <div className={cn(\"flex flex-col gap-1.5 p-6\", className)} {...props} />;\n}\n\nexport function CardTitle({ className, ...props }: HTMLAttributes<HTMLHeadingElement>) {\n  return <h3 className={cn(\"text-base font-semibold leading-none tracking-tight\", className)} {...props} />;\n}\n\nexport function CardDescription({ className, ...props }: HTMLAttributes<HTMLParagraphElement>) {\n  return <p className={cn(\"text-sm text-muted-foreground\", className)} {...props} />;\n}\n\nexport function CardContent({ className, ...props }: HTMLAttributes<HTMLDivElement>) {\n  return <div className={cn(\"p-6 pt-0\", className)} {...props} />;\n}\n\nexport function CardFooter({ className, ...props }: HTMLAttributes<HTMLDivElement>) {\n  return <div className={cn(\"flex items-center gap-2 p-6 pt-0\", className)} {...props} />;\n}\n"
    },
    {
      "name": "Input, Textarea, Select, Field",
      "filename": "components/ui/input.tsx",
      "source": "import {\n  forwardRef,\n  type InputHTMLAttributes,\n  type LabelHTMLAttributes,\n  type ReactNode,\n  type SelectHTMLAttributes,\n  type TextareaHTMLAttributes,\n} from \"react\";\nimport { ChevronDown } from \"lucide-react\";\nimport { cn } from \"./cn\";\n\nconst fieldBase =\n  \"w-full rounded-md border border-input bg-background text-sm text-foreground shadow-xs transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/30 disabled:cursor-not-allowed disabled:opacity-50\";\n\nexport function Label({ className, ...props }: LabelHTMLAttributes<HTMLLabelElement>) {\n  return <label className={cn(\"text-sm font-medium text-foreground\", className)} {...props} />;\n}\n\nexport interface InputProps extends InputHTMLAttributes<HTMLInputElement> {\n  invalid?: boolean;\n  icon?: ReactNode;\n}\n\nexport const Input = forwardRef<HTMLInputElement, InputProps>(function Input(\n  { className, invalid, icon, ...props },\n  ref,\n) {\n  if (icon) {\n    return (\n      <div className=\"relative\">\n        <span className=\"pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3 text-muted-foreground\">\n          {icon}\n        </span>\n        <input\n          ref={ref}\n          className={cn(fieldBase, \"h-10 pl-9 pr-3\", invalid && \"border-destructive focus-visible:ring-destructive/30\", className)}\n          {...props}\n        />\n      </div>\n    );\n  }\n  return (\n    <input\n      ref={ref}\n      className={cn(fieldBase, \"h-10 px-3\", invalid && \"border-destructive focus-visible:ring-destructive/30\", className)}\n      {...props}\n    />\n  );\n});\n\nexport const Textarea = forwardRef<HTMLTextAreaElement, TextareaHTMLAttributes<HTMLTextAreaElement> & { invalid?: boolean }>(\n  function Textarea({ className, invalid, ...props }, ref) {\n    return (\n      <textarea\n        ref={ref}\n        className={cn(fieldBase, \"min-h-20 px-3 py-2\", invalid && \"border-destructive focus-visible:ring-destructive/30\", className)}\n        {...props}\n      />\n    );\n  },\n);\n\nexport const Select = forwardRef<HTMLSelectElement, SelectHTMLAttributes<HTMLSelectElement> & { invalid?: boolean }>(\n  function Select({ className, invalid, children, ...props }, ref) {\n    return (\n      <div className=\"relative\">\n        <select\n          ref={ref}\n          className={cn(fieldBase, \"h-10 appearance-none pl-3 pr-9\", invalid && \"border-destructive\", className)}\n          {...props}\n        >\n          {children}\n        </select>\n        <ChevronDown className=\"pointer-events-none absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground\" />\n      </div>\n    );\n  },\n);\n\n// Label + control + hint/error wrapper.\nexport function Field({\n  label,\n  htmlFor,\n  hint,\n  error,\n  children,\n  className,\n}: {\n  label?: string;\n  htmlFor?: string;\n  hint?: string;\n  error?: string;\n  children: ReactNode;\n  className?: string;\n}) {\n  return (\n    <div className={cn(\"flex flex-col gap-1.5\", className)}>\n      {label && <Label htmlFor={htmlFor}>{label}</Label>}\n      {children}\n      {error ? (\n        <p className=\"text-xs text-destructive\">{error}</p>\n      ) : hint ? (\n        <p className=\"text-xs text-muted-foreground\">{hint}</p>\n      ) : null}\n    </div>\n  );\n}\n"
    },
    {
      "name": "Checkbox, Radio, Switch",
      "filename": "components/ui/toggle.tsx",
      "source": "import { type ReactNode } from \"react\";\nimport { Check } from \"lucide-react\";\nimport { cn } from \"./cn\";\n\nexport function Checkbox({\n  checked,\n  onChange,\n  label,\n  disabled,\n  className,\n}: {\n  checked: boolean;\n  onChange: (checked: boolean) => void;\n  label?: ReactNode;\n  disabled?: boolean;\n  className?: string;\n}) {\n  return (\n    <label\n      className={cn(\n        \"inline-flex cursor-pointer select-none items-center gap-2\",\n        disabled && \"cursor-not-allowed opacity-50\",\n        className,\n      )}\n    >\n      <span\n        className={cn(\n          \"flex h-4 w-4 items-center justify-center rounded border transition-colors\",\n          checked ? \"border-primary bg-primary text-primary-foreground\" : \"border-input bg-background\",\n        )}\n      >\n        {checked && <Check className=\"h-3 w-3\" strokeWidth={3} />}\n      </span>\n      <input\n        type=\"checkbox\"\n        className=\"sr-only\"\n        checked={checked}\n        disabled={disabled}\n        onChange={(e) => onChange(e.target.checked)}\n      />\n      {label && <span className=\"text-sm text-foreground\">{label}</span>}\n    </label>\n  );\n}\n\nexport function Radio({\n  checked,\n  onChange,\n  label,\n  disabled,\n  className,\n}: {\n  checked: boolean;\n  onChange: (checked: boolean) => void;\n  label?: ReactNode;\n  disabled?: boolean;\n  className?: string;\n}) {\n  return (\n    <label\n      className={cn(\n        \"inline-flex cursor-pointer select-none items-center gap-2\",\n        disabled && \"cursor-not-allowed opacity-50\",\n        className,\n      )}\n    >\n      <span\n        className={cn(\n          \"flex h-4 w-4 items-center justify-center rounded-full border transition-colors\",\n          checked ? \"border-primary\" : \"border-input bg-background\",\n        )}\n      >\n        {checked && <span className=\"h-2 w-2 rounded-full bg-primary\" />}\n      </span>\n      <input\n        type=\"radio\"\n        className=\"sr-only\"\n        checked={checked}\n        disabled={disabled}\n        onChange={(e) => onChange(e.target.checked)}\n      />\n      {label && <span className=\"text-sm text-foreground\">{label}</span>}\n    </label>\n  );\n}\n\nexport function Switch({\n  checked,\n  onChange,\n  disabled,\n  className,\n  \"aria-label\": ariaLabel,\n}: {\n  checked: boolean;\n  onChange: (checked: boolean) => void;\n  disabled?: boolean;\n  className?: string;\n  \"aria-label\"?: string;\n}) {\n  return (\n    <button\n      type=\"button\"\n      role=\"switch\"\n      aria-checked={checked}\n      aria-label={ariaLabel}\n      disabled={disabled}\n      onClick={() => onChange(!checked)}\n      className={cn(\n        \"relative inline-flex h-5 w-9 shrink-0 items-center rounded-full transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50\",\n        checked ? \"bg-primary\" : \"bg-muted\",\n        className,\n      )}\n    >\n      <span\n        className={cn(\n          \"inline-block h-4 w-4 transform rounded-full bg-white shadow-sm transition-transform\",\n          checked ? \"translate-x-4\" : \"translate-x-0.5\",\n        )}\n      />\n    </button>\n  );\n}\n"
    },
    {
      "name": "Avatar",
      "filename": "components/ui/avatar.tsx",
      "source": "import { cn } from \"./cn\";\n\nconst sizes = {\n  sm: \"h-7 w-7 text-[11px]\",\n  md: \"h-9 w-9 text-xs\",\n  lg: \"h-12 w-12 text-sm\",\n} as const;\n\nfunction initials(name: string): string {\n  return name\n    .split(\" \")\n    .map((p) => p[0])\n    .filter(Boolean)\n    .slice(0, 2)\n    .join(\"\")\n    .toUpperCase();\n}\n\nexport function Avatar({\n  name,\n  src,\n  size = \"md\",\n  className,\n}: {\n  name: string;\n  src?: string;\n  size?: keyof typeof sizes;\n  className?: string;\n}) {\n  return (\n    <span\n      className={cn(\n        \"inline-flex shrink-0 items-center justify-center overflow-hidden rounded-full bg-primary/10 font-semibold text-primary ring-2 ring-card\",\n        sizes[size],\n        className,\n      )}\n    >\n      {src ? (\n        <img src={src} alt={name} className=\"h-full w-full object-cover\" />\n      ) : (\n        <span>{initials(name)}</span>\n      )}\n    </span>\n  );\n}\n\nexport function AvatarGroup({ names, max = 4, size = \"md\" }: { names: string[]; max?: number; size?: keyof typeof sizes }) {\n  const shown = names.slice(0, max);\n  const extra = names.length - shown.length;\n  return (\n    <div className=\"flex -space-x-2\">\n      {shown.map((n, i) => (\n        <Avatar key={i} name={n} size={size} />\n      ))}\n      {extra > 0 && (\n        <span\n          className={cn(\n            \"inline-flex items-center justify-center rounded-full bg-muted font-semibold text-muted-foreground ring-2 ring-card\",\n            sizes[size],\n          )}\n        >\n          +{extra}\n        </span>\n      )}\n    </div>\n  );\n}\n"
    },
    {
      "name": "Tabs",
      "filename": "components/ui/tabs.tsx",
      "source": "import { useState, type ReactNode } from \"react\";\nimport { cn } from \"./cn\";\n\nexport interface TabItem {\n  label: string;\n  content: ReactNode;\n}\n\nexport function Tabs({ tabs, className }: { tabs: TabItem[]; className?: string }) {\n  const [active, setActive] = useState(0);\n  return (\n    <div className={className}>\n      <div role=\"tablist\" className=\"flex gap-1 border-b border-border\">\n        {tabs.map((tab, i) => (\n          <button\n            key={tab.label}\n            role=\"tab\"\n            aria-selected={i === active}\n            onClick={() => setActive(i)}\n            className={cn(\n              \"relative -mb-px border-b-2 px-3 py-2.5 text-sm font-medium transition-colors focus-visible:outline-none\",\n              i === active\n                ? \"border-primary text-foreground\"\n                : \"border-transparent text-muted-foreground hover:text-foreground\",\n            )}\n          >\n            {tab.label}\n          </button>\n        ))}\n      </div>\n      <div role=\"tabpanel\" className=\"pt-5\">\n        {tabs[active]?.content}\n      </div>\n    </div>\n  );\n}\n"
    },
    {
      "name": "Alert, Progress, Skeleton, Tooltip",
      "filename": "components/ui/feedback.tsx",
      "source": "import { type ReactNode } from \"react\";\nimport { Info, CheckCircle2, AlertTriangle, XCircle } from \"lucide-react\";\nimport { cn } from \"./cn\";\n\n// ---- Alert -----------------------------------------------------------------\nexport type AlertVariant = \"info\" | \"success\" | \"warning\" | \"destructive\";\n\nconst alertStyles: Record<AlertVariant, { wrap: string; icon: ReactNode }> = {\n  info: { wrap: \"border-primary/30 bg-primary/[0.06] text-foreground\", icon: <Info className=\"h-4 w-4 text-primary\" /> },\n  success: { wrap: \"border-success/30 bg-success/[0.08] text-foreground\", icon: <CheckCircle2 className=\"h-4 w-4 text-success\" /> },\n  warning: { wrap: \"border-amber-500/30 bg-amber-500/[0.08] text-foreground\", icon: <AlertTriangle className=\"h-4 w-4 text-amber-500\" /> },\n  destructive: { wrap: \"border-destructive/30 bg-destructive/[0.07] text-foreground\", icon: <XCircle className=\"h-4 w-4 text-destructive\" /> },\n};\n\nexport function Alert({\n  variant = \"info\",\n  title,\n  children,\n  className,\n}: {\n  variant?: AlertVariant;\n  title?: string;\n  children?: ReactNode;\n  className?: string;\n}) {\n  const s = alertStyles[variant];\n  return (\n    <div role=\"alert\" className={cn(\"flex gap-3 rounded-lg border p-4\", s.wrap, className)}>\n      <span className=\"mt-0.5 shrink-0\">{s.icon}</span>\n      <div className=\"min-w-0 text-sm\">\n        {title && <p className=\"font-semibold\">{title}</p>}\n        {children && <div className={cn(\"text-muted-foreground\", title && \"mt-0.5\")}>{children}</div>}\n      </div>\n    </div>\n  );\n}\n\n// ---- Progress --------------------------------------------------------------\nexport function Progress({\n  value,\n  className,\n  tone = \"primary\",\n}: {\n  value: number;\n  className?: string;\n  tone?: \"primary\" | \"success\" | \"warning\" | \"destructive\";\n}) {\n  const v = Math.min(100, Math.max(0, value));\n  const bar = {\n    primary: \"bg-primary\",\n    success: \"bg-success\",\n    warning: \"bg-amber-500\",\n    destructive: \"bg-destructive\",\n  }[tone];\n  return (\n    <div className={cn(\"h-2 w-full overflow-hidden rounded-full bg-muted\", className)}>\n      <div className={cn(\"h-full rounded-full transition-all\", bar)} style={{ width: `${v}%` }} />\n    </div>\n  );\n}\n\n// ---- Skeleton --------------------------------------------------------------\nexport function Skeleton({ className }: { className?: string }) {\n  return <div className={cn(\"animate-pulse rounded-md bg-muted\", className)} />;\n}\n\n// ---- Tooltip (CSS hover) ---------------------------------------------------\nexport function Tooltip({ label, children }: { label: string; children: ReactNode }) {\n  return (\n    <span className=\"group relative inline-flex\">\n      {children}\n      <span\n        role=\"tooltip\"\n        className=\"pointer-events-none absolute -top-1.5 left-1/2 z-50 -translate-x-1/2 -translate-y-full whitespace-nowrap rounded-md bg-foreground px-2 py-1 text-xs font-medium text-background opacity-0 shadow-md transition-opacity group-hover:opacity-100\"\n      >\n        {label}\n      </span>\n    </span>\n  );\n}\n"
    }
  ]
}