Prechádzať zdrojové kódy

feat: shadcn-svelte renderer (#227)

* init

* fix: resolve all type errors, runtime bugs, and test failures in @json-render/shadcn-svelte

- Add /index.js extensions to all ../ui/* imports (NodeNext moduleResolution)
- Type shadcnComponents as Record<string, Component<any>> to fix d.ts generation
- Call getBoundProp() once at top level instead of inside a binding() function
  called from event handlers (getContext() only works during initialization)
- Move getOptionalValidationContext() call to top level; update createValidation()
  to accept the context as first param instead of calling getContext() itself
- Wrap validation.register() in untrack() to break the fieldConfigs read-write
  cycle that caused effect_update_depth_exceeded in tests
- Fix implicit any on Input onkeydown handler (KeyboardEvent type)
- Use untrack() for $state() initializers that read reactive props values
- Call getStateValue() once at top level in Dialog/Drawer (not in a function
  called from event handlers)
- Fix test fixtures: use named imports { StateProvider, ValidationProvider }
- Add server.deps.inline for bits-ui and @lucide/svelte so vitest can process
  their .svelte source files; add root svelte.config.js with runes: true
- Add role/aria-modal/tabindex/onkeydown a11y attributes to Dialog and Drawer
- Fix self-closing non-void elements in Skeleton, Spinner, Textarea

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* update shadcn-svelte components

* fixes

* fixes

* fix: address PR review issues for shadcn-svelte renderer

- Fix missing comma in .changeset/config.json (invalid JSON)
- Fix Slider.svelte type errors (single-mode expects number, not array)
- Replace raw <button> with shadcn Button in Pagination.svelte
- Remove global font import and body/html overrides from app.css
- Remove root svelte.config.js (duplicates package-level config)
- Revert unrelated packageManager version bump

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
maxffarrell 3 mesiacov pred
rodič
commit
40892a6af0
100 zmenil súbory, kde vykonal 3511 pridanie a 0 odobranie
  1. 1 0
      .changeset/config.json
  2. 70 0
      packages/shadcn-svelte/README.md
  3. 20 0
      packages/shadcn-svelte/components.json
  4. 87 0
      packages/shadcn-svelte/package.json
  5. 20 0
      packages/shadcn-svelte/src/Accordion.svelte
  6. 18 0
      packages/shadcn-svelte/src/Alert.svelte
  7. 29 0
      packages/shadcn-svelte/src/Avatar.svelte
  8. 11 0
      packages/shadcn-svelte/src/Badge.svelte
  9. 22 0
      packages/shadcn-svelte/src/Button.svelte
  10. 36 0
      packages/shadcn-svelte/src/ButtonGroup.svelte
  11. 39 0
      packages/shadcn-svelte/src/Card.svelte
  12. 38 0
      packages/shadcn-svelte/src/Carousel.svelte
  13. 60 0
      packages/shadcn-svelte/src/Checkbox.svelte
  14. 39 0
      packages/shadcn-svelte/src/Collapsible.svelte
  15. 39 0
      packages/shadcn-svelte/src/Dialog.svelte
  16. 41 0
      packages/shadcn-svelte/src/Drawer.svelte
  17. 41 0
      packages/shadcn-svelte/src/DropdownMenu.svelte
  18. 32 0
      packages/shadcn-svelte/src/Grid.svelte
  19. 21 0
      packages/shadcn-svelte/src/Heading.svelte
  20. 25 0
      packages/shadcn-svelte/src/Image.svelte
  21. 70 0
      packages/shadcn-svelte/src/Input.svelte
  22. 22 0
      packages/shadcn-svelte/src/Link.svelte
  23. 47 0
      packages/shadcn-svelte/src/Pagination.svelte
  24. 23 0
      packages/shadcn-svelte/src/Popover.svelte
  25. 19 0
      packages/shadcn-svelte/src/Progress.svelte
  26. 63 0
      packages/shadcn-svelte/src/Radio.svelte
  27. 63 0
      packages/shadcn-svelte/src/Select.svelte
  28. 14 0
      packages/shadcn-svelte/src/Separator.svelte
  29. 13 0
      packages/shadcn-svelte/src/Skeleton.svelte
  30. 44 0
      packages/shadcn-svelte/src/Slider.svelte
  31. 19 0
      packages/shadcn-svelte/src/Spinner.svelte
  32. 39 0
      packages/shadcn-svelte/src/Stack.svelte
  33. 57 0
      packages/shadcn-svelte/src/Switch.svelte
  34. 36 0
      packages/shadcn-svelte/src/Table.svelte
  35. 49 0
      packages/shadcn-svelte/src/Tabs.svelte
  36. 22 0
      packages/shadcn-svelte/src/Text.svelte
  37. 65 0
      packages/shadcn-svelte/src/Textarea.svelte
  38. 28 0
      packages/shadcn-svelte/src/Toggle.svelte
  39. 52 0
      packages/shadcn-svelte/src/ToggleGroup.svelte
  40. 23 0
      packages/shadcn-svelte/src/Tooltip.svelte
  41. 69 0
      packages/shadcn-svelte/src/app.css
  42. 506 0
      packages/shadcn-svelte/src/catalog.ts
  43. 77 0
      packages/shadcn-svelte/src/components.ts
  44. 62 0
      packages/shadcn-svelte/src/helpers.ts
  45. 7 0
      packages/shadcn-svelte/src/index.ts
  46. 17 0
      packages/shadcn-svelte/src/lib/utils.ts
  47. 27 0
      packages/shadcn-svelte/src/ui/accordion/accordion-content.svelte
  48. 17 0
      packages/shadcn-svelte/src/ui/accordion/accordion-item.svelte
  49. 32 0
      packages/shadcn-svelte/src/ui/accordion/accordion-trigger.svelte
  50. 19 0
      packages/shadcn-svelte/src/ui/accordion/accordion.svelte
  51. 16 0
      packages/shadcn-svelte/src/ui/accordion/index.ts
  52. 20 0
      packages/shadcn-svelte/src/ui/alert/alert-action.svelte
  53. 23 0
      packages/shadcn-svelte/src/ui/alert/alert-description.svelte
  54. 23 0
      packages/shadcn-svelte/src/ui/alert/alert-title.svelte
  55. 43 0
      packages/shadcn-svelte/src/ui/alert/alert.svelte
  56. 17 0
      packages/shadcn-svelte/src/ui/alert/index.ts
  57. 49 0
      packages/shadcn-svelte/src/ui/badge/badge.svelte
  58. 2 0
      packages/shadcn-svelte/src/ui/badge/index.ts
  59. 82 0
      packages/shadcn-svelte/src/ui/button/button.svelte
  60. 17 0
      packages/shadcn-svelte/src/ui/button/index.ts
  61. 23 0
      packages/shadcn-svelte/src/ui/card/card-action.svelte
  62. 20 0
      packages/shadcn-svelte/src/ui/card/card-content.svelte
  63. 20 0
      packages/shadcn-svelte/src/ui/card/card-description.svelte
  64. 20 0
      packages/shadcn-svelte/src/ui/card/card-footer.svelte
  65. 23 0
      packages/shadcn-svelte/src/ui/card/card-header.svelte
  66. 15 0
      packages/shadcn-svelte/src/ui/card/card-title.svelte
  67. 22 0
      packages/shadcn-svelte/src/ui/card/card.svelte
  68. 25 0
      packages/shadcn-svelte/src/ui/card/index.ts
  69. 43 0
      packages/shadcn-svelte/src/ui/carousel/carousel-content.svelte
  70. 30 0
      packages/shadcn-svelte/src/ui/carousel/carousel-item.svelte
  71. 39 0
      packages/shadcn-svelte/src/ui/carousel/carousel-next.svelte
  72. 39 0
      packages/shadcn-svelte/src/ui/carousel/carousel-previous.svelte
  73. 94 0
      packages/shadcn-svelte/src/ui/carousel/carousel.svelte
  74. 60 0
      packages/shadcn-svelte/src/ui/carousel/context.ts
  75. 19 0
      packages/shadcn-svelte/src/ui/carousel/index.ts
  76. 39 0
      packages/shadcn-svelte/src/ui/checkbox/checkbox.svelte
  77. 6 0
      packages/shadcn-svelte/src/ui/checkbox/index.ts
  78. 7 0
      packages/shadcn-svelte/src/ui/collapsible/collapsible-content.svelte
  79. 7 0
      packages/shadcn-svelte/src/ui/collapsible/collapsible-trigger.svelte
  80. 11 0
      packages/shadcn-svelte/src/ui/collapsible/collapsible.svelte
  81. 13 0
      packages/shadcn-svelte/src/ui/collapsible/index.ts
  82. 11 0
      packages/shadcn-svelte/src/ui/dialog/dialog-close.svelte
  83. 48 0
      packages/shadcn-svelte/src/ui/dialog/dialog-content.svelte
  84. 17 0
      packages/shadcn-svelte/src/ui/dialog/dialog-description.svelte
  85. 32 0
      packages/shadcn-svelte/src/ui/dialog/dialog-footer.svelte
  86. 20 0
      packages/shadcn-svelte/src/ui/dialog/dialog-header.svelte
  87. 17 0
      packages/shadcn-svelte/src/ui/dialog/dialog-overlay.svelte
  88. 7 0
      packages/shadcn-svelte/src/ui/dialog/dialog-portal.svelte
  89. 17 0
      packages/shadcn-svelte/src/ui/dialog/dialog-title.svelte
  90. 11 0
      packages/shadcn-svelte/src/ui/dialog/dialog-trigger.svelte
  91. 7 0
      packages/shadcn-svelte/src/ui/dialog/dialog.svelte
  92. 34 0
      packages/shadcn-svelte/src/ui/dialog/index.ts
  93. 7 0
      packages/shadcn-svelte/src/ui/drawer/drawer-close.svelte
  94. 33 0
      packages/shadcn-svelte/src/ui/drawer/drawer-content.svelte
  95. 17 0
      packages/shadcn-svelte/src/ui/drawer/drawer-description.svelte
  96. 20 0
      packages/shadcn-svelte/src/ui/drawer/drawer-footer.svelte
  97. 20 0
      packages/shadcn-svelte/src/ui/drawer/drawer-header.svelte
  98. 12 0
      packages/shadcn-svelte/src/ui/drawer/drawer-nested.svelte
  99. 17 0
      packages/shadcn-svelte/src/ui/drawer/drawer-overlay.svelte
  100. 7 0
      packages/shadcn-svelte/src/ui/drawer/drawer-portal.svelte

+ 1 - 0
.changeset/config.json

@@ -23,6 +23,7 @@
       "@json-render/solid",
       "@json-render/react-three-fiber",
       "@json-render/yaml",
+      "@json-render/shadcn-svelte",
       "@json-render/ink"
     ]
   ],

+ 70 - 0
packages/shadcn-svelte/README.md

@@ -0,0 +1,70 @@
+# @json-render/shadcn-svelte
+
+Pre-built [shadcn-svelte](https://www.shadcn-svelte.com/) components for json-render. Drop-in catalog definitions and Svelte implementations for 36 components built on Svelte 5 + Tailwind CSS.
+
+## Installation
+
+```bash
+npm install @json-render/shadcn-svelte @json-render/core @json-render/svelte zod
+```
+
+## Quick Start
+
+### 1. Create a Catalog
+
+```ts
+import { schema } from "@json-render/svelte/schema";
+import { shadcnComponentDefinitions } from "@json-render/shadcn-svelte/catalog";
+
+const catalog = schema.createCatalog({
+  components: {
+    Card: shadcnComponentDefinitions.Card,
+    Stack: shadcnComponentDefinitions.Stack,
+    Heading: shadcnComponentDefinitions.Heading,
+    Button: shadcnComponentDefinitions.Button,
+    Input: shadcnComponentDefinitions.Input,
+  },
+  actions: {},
+});
+```
+
+### 2. Create a Registry
+
+```ts
+import { defineRegistry } from "@json-render/svelte";
+import { shadcnComponents } from "@json-render/shadcn-svelte";
+
+const { registry } = defineRegistry(catalog, {
+  components: {
+    Card: shadcnComponents.Card,
+    Stack: shadcnComponents.Stack,
+    Heading: shadcnComponents.Heading,
+    Button: shadcnComponents.Button,
+    Input: shadcnComponents.Input,
+  },
+});
+```
+
+### 3. Render
+
+```svelte
+<script lang="ts">
+  import { Renderer, JsonUIProvider } from "@json-render/svelte";
+
+  export let spec;
+  export let registry;
+</script>
+
+<JsonUIProvider initialState={spec?.state ?? {}}>
+  <Renderer {spec} {registry} />
+</JsonUIProvider>
+```
+
+## Exports
+
+| Entry Point | Exports |
+|-------------|---------|
+| `@json-render/shadcn-svelte` | `shadcnComponents`, `shadcnComponentDefinitions` |
+| `@json-render/shadcn-svelte/catalog` | `shadcnComponentDefinitions` |
+
+The `/catalog` entrypoint contains only Zod schemas (no renderer dependency), so it can be used in server-side code for prompt generation.

+ 20 - 0
packages/shadcn-svelte/components.json

@@ -0,0 +1,20 @@
+{
+	"$schema": "https://shadcn-svelte.com/schema.json",
+	"tailwind": {
+		"css": "src/app.css",
+		"baseColor": "neutral"
+	},
+	"aliases": {
+		"components": "$ui",
+		"utils": "$lib/utils",
+		"ui": "$ui",
+		"hooks": "$lib/hooks",
+		"lib": "$lib"
+	},
+	"typescript": true,
+	"registry": "https://shadcn-svelte.com/registry",
+	"style": "vega",
+	"iconLibrary": "lucide",
+	"menuColor": "default",
+	"menuAccent": "subtle"
+}

+ 87 - 0
packages/shadcn-svelte/package.json

@@ -0,0 +1,87 @@
+{
+  "name": "@json-render/shadcn-svelte",
+  "version": "0.1.0",
+  "license": "Apache-2.0",
+  "description": "shadcn-svelte component library for @json-render/svelte. JSON becomes beautiful Tailwind-styled Svelte components.",
+  "keywords": [
+    "json",
+    "ui",
+    "svelte",
+    "svelte5",
+    "shadcn-svelte",
+    "tailwind",
+    "bits-ui",
+    "ai",
+    "generative-ui",
+    "llm",
+    "renderer",
+    "streaming",
+    "components"
+  ],
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/vercel-labs/json-render.git",
+    "directory": "packages/shadcn-svelte"
+  },
+  "homepage": "https://github.com/vercel-labs/json-render#readme",
+  "bugs": {
+    "url": "https://github.com/vercel-labs/json-render/issues"
+  },
+  "publishConfig": {
+    "access": "public"
+  },
+  "type": "module",
+  "svelte": "./dist/index.js",
+  "main": "./dist/index.js",
+  "module": "./dist/index.js",
+  "types": "./dist/index.d.ts",
+  "exports": {
+    ".": {
+      "types": "./dist/index.d.ts",
+      "svelte": "./dist/index.js",
+      "default": "./dist/index.js"
+    },
+    "./catalog": {
+      "types": "./dist/catalog.d.ts",
+      "svelte": "./dist/catalog.js",
+      "default": "./dist/catalog.js"
+    }
+  },
+  "files": [
+    "dist"
+  ],
+  "scripts": {
+    "build": "svelte-package -i src -o dist",
+    "dev": "svelte-package -i src -o dist --watch",
+    "check-types": "svelte-check --tsconfig ./tsconfig.json",
+    "typecheck": "svelte-check --tsconfig ./tsconfig.json"
+  },
+  "dependencies": {
+    "@json-render/core": "workspace:*",
+    "@json-render/svelte": "workspace:*",
+    "@lucide/svelte": "^0.577.0",
+    "bits-ui": "^2.16.3",
+    "clsx": "^2.1.1",
+    "embla-carousel-svelte": "^8.6.0",
+    "tailwind-merge": "^3.5.0",
+    "tailwind-variants": "^3.2.2",
+    "vaul-svelte": "1.0.0-next.7",
+    "zod": "^4.3.6"
+  },
+  "devDependencies": {
+    "@fontsource-variable/inter": "^5.2.8",
+    "@internal/typescript-config": "workspace:*",
+    "@internationalized/date": "^3.12.0",
+    "@sveltejs/package": "^2.5.7",
+    "@sveltejs/vite-plugin-svelte": "^7.0.0",
+    "svelte": "^5.54.1",
+    "svelte-check": "^4.4.5",
+    "tw-animate-css": "^1.4.0",
+    "typescript": "^5.9.3"
+  },
+  "peerDependencies": {
+    "svelte": "^5.0.0",
+    "tailwindcss": "^4.0.0",
+    "zod": "^4.0.0"
+  }
+}

+ 20 - 0
packages/shadcn-svelte/src/Accordion.svelte

@@ -0,0 +1,20 @@
+<script lang="ts">
+  import type { BaseComponentProps } from "@json-render/svelte";
+  import type { ShadcnProps } from "./catalog.js";
+  import * as Accordion from "./ui/accordion/index.js";
+
+  interface Props extends BaseComponentProps<ShadcnProps<"Accordion">> {}
+
+  let { props }: Props = $props();
+
+  const items = $derived(props.items ?? []);
+</script>
+
+<Accordion.Root type={props.type ?? "single"} class="w-full">
+  {#each items as item, i}
+    <Accordion.Item value={`item-${i}`}>
+      <Accordion.Trigger>{item.title}</Accordion.Trigger>
+      <Accordion.Content>{item.content}</Accordion.Content>
+    </Accordion.Item>
+  {/each}
+</Accordion.Root>

+ 18 - 0
packages/shadcn-svelte/src/Alert.svelte

@@ -0,0 +1,18 @@
+<script lang="ts">
+  import type { BaseComponentProps } from "@json-render/svelte";
+  import type { ShadcnProps } from "./catalog.js";
+  import { Alert, AlertDescription, AlertTitle } from "./ui/alert/index.js";
+
+  interface Props extends BaseComponentProps<ShadcnProps<"Alert">> {}
+
+  let { props }: Props = $props();
+
+  const variant = $derived(props.type === "error" ? "destructive" : "default");
+</script>
+
+<Alert {variant}>
+  <AlertTitle>{props.title}</AlertTitle>
+  {#if props.message}
+    <AlertDescription>{props.message}</AlertDescription>
+  {/if}
+</Alert>

+ 29 - 0
packages/shadcn-svelte/src/Avatar.svelte

@@ -0,0 +1,29 @@
+<script lang="ts">
+  import type { BaseComponentProps } from "@json-render/svelte";
+  import type { ShadcnProps } from "./catalog.js";
+
+  interface Props extends BaseComponentProps<ShadcnProps<"Avatar">> {}
+
+  let { props }: Props = $props();
+
+  const initials = $derived(
+    props.name
+      .split(" ")
+      .map((part) => part[0] ?? "")
+      .slice(0, 2)
+      .join("")
+      .toUpperCase(),
+  );
+
+  const sizeClass = $derived(
+    props.size === "sm" ? "size-8 text-xs" : props.size === "lg" ? "size-12 text-base" : "size-10 text-sm",
+  );
+</script>
+
+<div class={`inline-flex items-center justify-center overflow-hidden rounded-full bg-muted text-muted-foreground ${sizeClass}`}>
+  {#if props.src}
+    <img src={props.src} alt={props.name} class="h-full w-full object-cover" />
+  {:else}
+    <span>{initials}</span>
+  {/if}
+</div>

+ 11 - 0
packages/shadcn-svelte/src/Badge.svelte

@@ -0,0 +1,11 @@
+<script lang="ts">
+  import type { BaseComponentProps } from "@json-render/svelte";
+  import type { ShadcnProps } from "./catalog.js";
+  import { Badge } from "./ui/badge/index.js";
+
+  interface Props extends BaseComponentProps<ShadcnProps<"Badge">> {}
+
+  let { props }: Props = $props();
+</script>
+
+<Badge variant={props.variant ?? "default"}>{props.text}</Badge>

+ 22 - 0
packages/shadcn-svelte/src/Button.svelte

@@ -0,0 +1,22 @@
+<script lang="ts">
+  import type { BaseComponentProps } from "@json-render/svelte";
+  import type { ShadcnProps } from "./catalog.js";
+  import { Button } from "./ui/button/index.js";
+
+  interface Props extends BaseComponentProps<ShadcnProps<"Button">> {}
+
+  let { props, emit }: Props = $props();
+
+  const rawVariant = $derived((props.variant ?? "primary") as string);
+  const variant = $derived(
+    rawVariant === "danger" || rawVariant === "destructive"
+      ? "destructive"
+      : rawVariant === "secondary" || rawVariant === "outline" || rawVariant === "ghost"
+        ? "secondary"
+        : "default",
+  );
+</script>
+
+<Button {variant} disabled={props.disabled ?? false} onclick={() => emit("press")}>
+  {props.label}
+</Button>

+ 36 - 0
packages/shadcn-svelte/src/ButtonGroup.svelte

@@ -0,0 +1,36 @@
+<script lang="ts">
+  import { getBoundProp } from "@json-render/svelte";
+  import type { BaseComponentProps } from "@json-render/svelte";
+  import type { ShadcnProps } from "./catalog.js";
+
+  interface Props extends BaseComponentProps<ShadcnProps<"ButtonGroup">> {}
+
+  let { props, bindings, emit }: Props = $props();
+
+  let localValue = $state("");
+
+  const bound = getBoundProp<string>(
+    () => props.selected ?? undefined,
+    () => bindings?.selected,
+  );
+
+  const value = $derived(bound.current ?? localValue);
+
+  function choose(next: string) {
+    localValue = next;
+    bound.current = next;
+    emit("change");
+  }
+</script>
+
+<div class="inline-flex rounded-md border border-border">
+  {#each props.buttons ?? [] as btn, i}
+    <button
+      type="button"
+      class={`px-3 py-1.5 text-sm ${value === btn.value ? "bg-primary text-primary-foreground" : "bg-background hover:bg-muted"} ${i > 0 ? "border-l border-border" : ""}`}
+      onclick={() => choose(btn.value)}
+    >
+      {btn.label}
+    </button>
+  {/each}
+</div>

+ 39 - 0
packages/shadcn-svelte/src/Card.svelte

@@ -0,0 +1,39 @@
+<script lang="ts">
+  import type { Snippet } from "svelte";
+  import type { BaseComponentProps } from "@json-render/svelte";
+  import type { ShadcnProps } from "./catalog.js";
+  import * as Card from "./ui/card/index.js";
+  import { cn } from "./lib/utils.js";
+
+  interface Props extends BaseComponentProps<ShadcnProps<"Card">> {
+    children?: Snippet;
+  }
+
+  let { props, children }: Props = $props();
+
+  const maxWidthClass = $derived(
+    props.maxWidth === "sm"
+      ? "max-w-xs sm:min-w-[280px]"
+      : props.maxWidth === "md"
+        ? "max-w-sm sm:min-w-[320px]"
+        : props.maxWidth === "lg"
+          ? "max-w-md sm:min-w-[360px]"
+          : "w-full",
+  );
+</script>
+
+<Card.Root class={cn(maxWidthClass, props.centered ? "mx-auto" : "") }>
+  {#if props.title || props.description}
+    <Card.Header>
+      {#if props.title}
+        <Card.Title>{props.title}</Card.Title>
+      {/if}
+      {#if props.description}
+        <Card.Description>{props.description}</Card.Description>
+      {/if}
+    </Card.Header>
+  {/if}
+  <Card.Content class="flex flex-col gap-3">
+    {@render children?.()}
+  </Card.Content>
+</Card.Root>

+ 38 - 0
packages/shadcn-svelte/src/Carousel.svelte

@@ -0,0 +1,38 @@
+<script lang="ts">
+  import type { BaseComponentProps } from "@json-render/svelte";
+  import type { ShadcnProps } from "./catalog.js";
+  import {
+    Carousel,
+    CarouselContent,
+    CarouselItem,
+    CarouselNext,
+    CarouselPrevious,
+  } from "./ui/carousel/index.js";
+
+  interface Props extends BaseComponentProps<ShadcnProps<"Carousel">> {}
+
+  let { props }: Props = $props();
+
+  const items = $derived(props.items ?? []);
+</script>
+
+<div class="relative px-10">
+  <Carousel>
+    <CarouselContent>
+      {#each items as item}
+        <CarouselItem class="basis-64">
+          <div class="rounded-lg border bg-card p-4">
+            {#if item.title}
+              <h4 class="font-medium">{item.title}</h4>
+            {/if}
+            {#if item.description}
+              <p class="text-sm text-muted-foreground">{item.description}</p>
+            {/if}
+          </div>
+        </CarouselItem>
+      {/each}
+    </CarouselContent>
+    <CarouselPrevious />
+    <CarouselNext />
+  </Carousel>
+</div>

+ 60 - 0
packages/shadcn-svelte/src/Checkbox.svelte

@@ -0,0 +1,60 @@
+<script lang="ts">
+  import { untrack } from "svelte";
+  import { getBoundProp, getOptionalValidationContext } from "@json-render/svelte";
+  import type { BaseComponentProps } from "@json-render/svelte";
+  import type { ShadcnProps } from "./catalog.js";
+  import { createValidation } from "./helpers.js";
+  import { Checkbox } from "./ui/checkbox/index.js";
+  import { Label } from "./ui/label/index.js";
+
+  interface Props extends BaseComponentProps<ShadcnProps<"Checkbox">> {}
+
+  let { props, bindings, emit }: Props = $props();
+
+  let localChecked = $state(untrack(() => Boolean(props.checked)));
+  let errors = $state<string[]>([]);
+
+  const validateOn = $derived((props.validateOn ?? "change") as "change" | "blur" | "submit");
+  const validationCtx = getOptionalValidationContext();
+  const validation = createValidation(
+    validationCtx,
+    untrack(() => bindings?.checked),
+    untrack(() => props.checks ?? null),
+  );
+
+  $effect(() => {
+    const on = validateOn;
+    untrack(() => {
+      if (validation.hasValidation) validation.register(on);
+    });
+  });
+
+  const bound = getBoundProp<boolean>(
+    () => props.checked ?? undefined,
+    () => bindings?.checked,
+  );
+
+  const checked = $derived(bound.current ?? localChecked);
+
+  function handleChange(value: boolean | "indeterminate") {
+    const next = value === true;
+    localChecked = next;
+    bound.current = next;
+    if (validateOn === "change") errors = validation.run(validateOn);
+    emit("change");
+  }
+</script>
+
+<div class="space-y-1">
+  <div class="flex items-center gap-2">
+    <Checkbox
+      id={props.name}
+      {checked}
+      onCheckedChange={handleChange}
+    />
+    <Label for={props.name} class="text-sm cursor-pointer">{props.label}</Label>
+  </div>
+  {#if errors.length > 0}
+    <p class="text-sm text-destructive">{errors[0]}</p>
+  {/if}
+</div>

+ 39 - 0
packages/shadcn-svelte/src/Collapsible.svelte

@@ -0,0 +1,39 @@
+<script lang="ts">
+  import { untrack } from "svelte";
+  import type { Snippet } from "svelte";
+  import type { BaseComponentProps } from "@json-render/svelte";
+  import type { ShadcnProps } from "./catalog.js";
+  import {
+    Collapsible,
+    CollapsibleContent,
+    CollapsibleTrigger,
+  } from "./ui/collapsible/index.js";
+  import { Button } from "./ui/button/index.js";
+  import ChevronDown from "@lucide/svelte/icons/chevron-down";
+
+  interface Props extends BaseComponentProps<ShadcnProps<"Collapsible">> {
+    children?: Snippet;
+  }
+
+  let { props, children }: Props = $props();
+
+  let open = $state(untrack(() => props.defaultOpen ?? false));
+</script>
+
+<Collapsible bind:open class="w-full rounded-md border border-border">
+  <CollapsibleTrigger>
+    {#snippet child({ props: triggerProps })}
+      <Button
+        variant="ghost"
+        class="flex w-full items-center justify-between px-4 py-2 text-sm font-medium"
+        {...triggerProps}
+      >
+        <span>{props.title}</span>
+        <ChevronDown class={`size-4 transition-transform ${open ? "rotate-180" : ""}`} />
+      </Button>
+    {/snippet}
+  </CollapsibleTrigger>
+  <CollapsibleContent>
+    <div class="px-4 pb-3 pt-1">{@render children?.()}</div>
+  </CollapsibleContent>
+</Collapsible>

+ 39 - 0
packages/shadcn-svelte/src/Dialog.svelte

@@ -0,0 +1,39 @@
+<script lang="ts">
+  import { untrack } from "svelte";
+  import type { Snippet } from "svelte";
+  import { getStateValue } from "@json-render/svelte";
+  import type { BaseComponentProps } from "@json-render/svelte";
+  import type { ShadcnProps } from "./catalog.js";
+  import {
+    Dialog,
+    DialogContent,
+    DialogDescription,
+    DialogHeader,
+    DialogTitle,
+  } from "./ui/dialog/index.js";
+
+  interface Props extends BaseComponentProps<ShadcnProps<"Dialog">> {
+    children?: Snippet;
+  }
+
+  let { props, children }: Props = $props();
+
+  const openState = getStateValue(untrack(() => props.openPath));
+  const open = $derived(Boolean(openState.current));
+
+  function handleOpenChange(next: boolean) {
+    openState.current = next;
+  }
+</script>
+
+<Dialog {open} onOpenChange={handleOpenChange}>
+  <DialogContent>
+    <DialogHeader>
+      <DialogTitle>{props.title}</DialogTitle>
+      {#if props.description}
+        <DialogDescription>{props.description}</DialogDescription>
+      {/if}
+    </DialogHeader>
+    {@render children?.()}
+  </DialogContent>
+</Dialog>

+ 41 - 0
packages/shadcn-svelte/src/Drawer.svelte

@@ -0,0 +1,41 @@
+<script lang="ts">
+  import { untrack } from "svelte";
+  import type { Snippet } from "svelte";
+  import { getStateValue } from "@json-render/svelte";
+  import type { BaseComponentProps } from "@json-render/svelte";
+  import type { ShadcnProps } from "./catalog.js";
+  import {
+    Drawer,
+    DrawerContent,
+    DrawerDescription,
+    DrawerHeader,
+    DrawerTitle,
+  } from "./ui/drawer/index.js";
+
+  interface Props extends BaseComponentProps<ShadcnProps<"Drawer">> {
+    children?: Snippet;
+  }
+
+  let { props, children }: Props = $props();
+
+  const openState = getStateValue(untrack(() => props.openPath));
+  const open = $derived(Boolean(openState.current));
+
+  function handleOpenChange(next: boolean) {
+    openState.current = next;
+  }
+</script>
+
+<Drawer {open} onOpenChange={handleOpenChange}>
+  <DrawerContent>
+    <DrawerHeader>
+      <DrawerTitle>{props.title}</DrawerTitle>
+      {#if props.description}
+        <DrawerDescription>{props.description}</DrawerDescription>
+      {/if}
+    </DrawerHeader>
+    <div class="px-4 pb-4">
+      {@render children?.()}
+    </div>
+  </DrawerContent>
+</Drawer>

+ 41 - 0
packages/shadcn-svelte/src/DropdownMenu.svelte

@@ -0,0 +1,41 @@
+<script lang="ts">
+  import { getBoundProp } from "@json-render/svelte";
+  import type { BaseComponentProps } from "@json-render/svelte";
+  import type { ShadcnProps } from "./catalog.js";
+  import {
+    DropdownMenu,
+    DropdownMenuContent,
+    DropdownMenuItem,
+    DropdownMenuTrigger,
+  } from "./ui/dropdown-menu/index.js";
+  import { Button } from "./ui/button/index.js";
+
+  interface Props extends BaseComponentProps<ShadcnProps<"DropdownMenu">> {}
+
+  let { props, bindings, emit }: Props = $props();
+
+  const valueBound = getBoundProp<string>(
+    () => props.value ?? undefined,
+    () => bindings?.value,
+  );
+
+  function select(value: string) {
+    valueBound.current = value;
+    emit("select");
+  }
+</script>
+
+<DropdownMenu>
+  <DropdownMenuTrigger>
+    {#snippet child({ props: triggerProps })}
+      <Button variant="outline" {...triggerProps}>{props.label}</Button>
+    {/snippet}
+  </DropdownMenuTrigger>
+  <DropdownMenuContent>
+    {#each props.items ?? [] as item}
+      <DropdownMenuItem onclick={() => select(item.value)}>
+        {item.label}
+      </DropdownMenuItem>
+    {/each}
+  </DropdownMenuContent>
+</DropdownMenu>

+ 32 - 0
packages/shadcn-svelte/src/Grid.svelte

@@ -0,0 +1,32 @@
+<script lang="ts">
+  import type { Snippet } from "svelte";
+  import type { BaseComponentProps } from "@json-render/svelte";
+  import type { ShadcnProps } from "./catalog.js";
+
+  interface Props extends BaseComponentProps<ShadcnProps<"Grid">> {
+    children?: Snippet;
+  }
+
+  let { props, children }: Props = $props();
+
+  const colsMap: Record<number, string> = {
+    1: "grid-cols-1",
+    2: "grid-cols-2",
+    3: "grid-cols-3",
+    4: "grid-cols-4",
+    5: "grid-cols-5",
+    6: "grid-cols-6",
+  };
+
+  const gapMap: Record<string, string> = {
+    sm: "gap-2",
+    md: "gap-3",
+    lg: "gap-4",
+  };
+
+  const n = $derived(Math.max(1, Math.min(6, props.columns ?? 1)));
+</script>
+
+<div class={`grid ${colsMap[n] ?? "grid-cols-1"} ${gapMap[props.gap ?? "md"] ?? "gap-3"}`}>
+  {@render children?.()}
+</div>

+ 21 - 0
packages/shadcn-svelte/src/Heading.svelte

@@ -0,0 +1,21 @@
+<script lang="ts">
+  import type { BaseComponentProps } from "@json-render/svelte";
+  import type { ShadcnProps } from "./catalog.js";
+
+  interface Props extends BaseComponentProps<ShadcnProps<"Heading">> {}
+
+  let { props }: Props = $props();
+
+  const level = $derived(props.level ?? "h2");
+  const className = $derived(
+    level === "h1"
+      ? "text-3xl font-bold tracking-tight"
+      : level === "h2"
+        ? "text-2xl font-semibold tracking-tight"
+        : level === "h3"
+          ? "text-xl font-semibold"
+          : "text-lg font-medium",
+  );
+</script>
+
+<svelte:element this={level} class={className}>{props.text}</svelte:element>

+ 25 - 0
packages/shadcn-svelte/src/Image.svelte

@@ -0,0 +1,25 @@
+<script lang="ts">
+  import type { BaseComponentProps } from "@json-render/svelte";
+  import type { ShadcnProps } from "./catalog.js";
+
+  interface Props extends BaseComponentProps<ShadcnProps<"Image">> {}
+
+  let { props }: Props = $props();
+</script>
+
+{#if props.src}
+  <img
+    src={props.src}
+    alt={props.alt}
+    width={props.width ?? undefined}
+    height={props.height ?? undefined}
+    class="max-w-full rounded-md border border-border object-cover"
+  />
+{:else}
+  <div
+    class="flex items-center justify-center rounded-md border border-dashed border-border bg-muted text-sm text-muted-foreground"
+    style={`width:${props.width ?? 320}px;height:${props.height ?? 180}px`}
+  >
+    {props.alt}
+  </div>
+{/if}

+ 70 - 0
packages/shadcn-svelte/src/Input.svelte

@@ -0,0 +1,70 @@
+<script lang="ts">
+  import { untrack } from "svelte";
+  import { getBoundProp, getOptionalValidationContext } from "@json-render/svelte";
+  import type { BaseComponentProps } from "@json-render/svelte";
+  import type { ShadcnProps } from "./catalog.js";
+  import { Input } from "./ui/input/index.js";
+  import { Label } from "./ui/label/index.js";
+  import { createValidation } from "./helpers.js";
+
+  interface Props extends BaseComponentProps<ShadcnProps<"Input">> {}
+
+  let { props, bindings, emit }: Props = $props();
+
+  let localValue = $state("");
+  let errors = $state<string[]>([]);
+
+  const validateOn = $derived((props.validateOn ?? "blur") as "change" | "blur" | "submit");
+  const validationCtx = getOptionalValidationContext();
+  const validation = createValidation(
+    validationCtx,
+    untrack(() => bindings?.value),
+    untrack(() => props.checks ?? null),
+  );
+
+  $effect(() => {
+    const on = validateOn;
+    untrack(() => {
+      if (validation.hasValidation) validation.register(on);
+    });
+  });
+
+  const bound = getBoundProp<string>(
+    () => props.value ?? undefined,
+    () => bindings?.value,
+  );
+
+  const value = $derived(bound.current ?? localValue);
+
+  function handleInput(event: Event) {
+    const next = (event.target as HTMLInputElement).value;
+    localValue = next;
+    bound.current = next;
+    if (validateOn === "change") errors = validation.run(validateOn);
+  }
+
+  function handleBlur() {
+    if (validateOn === "blur") errors = validation.run(validateOn);
+    emit("blur");
+  }
+</script>
+
+<div class="space-y-2">
+  <Label for={props.name}>{props.label}</Label>
+  <Input
+    id={props.name}
+    name={props.name}
+    type={props.type ?? "text"}
+    placeholder={props.placeholder ?? ""}
+    {value}
+    oninput={handleInput}
+    onfocus={() => emit("focus")}
+    onblur={handleBlur}
+    onkeydown={(e: KeyboardEvent) => {
+      if (e.key === "Enter") emit("submit");
+    }}
+  />
+  {#if errors.length > 0}
+    <p class="text-sm text-destructive">{errors[0]}</p>
+  {/if}
+</div>

+ 22 - 0
packages/shadcn-svelte/src/Link.svelte

@@ -0,0 +1,22 @@
+<script lang="ts">
+  import type { BaseComponentProps } from "@json-render/svelte";
+  import type { ShadcnProps } from "./catalog.js";
+
+  interface Props extends BaseComponentProps<ShadcnProps<"Link">> {}
+
+  let { props, on }: Props = $props();
+
+  function handleClick(event: MouseEvent) {
+    const press = on("press");
+    if (press.shouldPreventDefault) event.preventDefault();
+    press.emit();
+  }
+</script>
+
+<a
+  href={props.href ?? "#"}
+  class="text-primary text-sm font-medium underline-offset-4 hover:underline"
+  onclick={handleClick}
+>
+  {props.label}
+</a>

+ 47 - 0
packages/shadcn-svelte/src/Pagination.svelte

@@ -0,0 +1,47 @@
+<script lang="ts">
+  import { untrack } from "svelte";
+  import { getBoundProp } from "@json-render/svelte";
+  import type { BaseComponentProps } from "@json-render/svelte";
+  import type { ShadcnProps } from "./catalog.js";
+  import { getPaginationRange } from "./helpers.js";
+  import { Button } from "./ui/button/index.js";
+
+  interface Props extends BaseComponentProps<ShadcnProps<"Pagination">> {}
+
+  let { props, bindings, emit }: Props = $props();
+
+  const pageBound = getBoundProp<number>(
+    () => props.page ?? undefined,
+    () => bindings?.page,
+  );
+
+  let localPage = $state(untrack(() => props.page ?? 1));
+  const page = $derived(pageBound.current ?? localPage);
+  const totalPages = $derived(props.totalPages ?? 1);
+  const pages = $derived(getPaginationRange(page, totalPages));
+
+  function setPage(next: number) {
+    if (next < 1 || next > totalPages || next === page) return;
+    localPage = next;
+    pageBound.current = next;
+    emit("change");
+  }
+</script>
+
+<div class="inline-flex items-center gap-1">
+  <Button variant="outline" size="sm" onclick={() => setPage(page - 1)} disabled={page <= 1}>Prev</Button>
+  {#each pages as p}
+    {#if p === "ellipsis"}
+      <span class="px-2 text-sm text-muted-foreground">…</span>
+    {:else}
+      <Button
+        variant={p === page ? "default" : "outline"}
+        size="sm"
+        onclick={() => setPage(p)}
+      >
+        {p}
+      </Button>
+    {/if}
+  {/each}
+  <Button variant="outline" size="sm" onclick={() => setPage(page + 1)} disabled={page >= totalPages}>Next</Button>
+</div>

+ 23 - 0
packages/shadcn-svelte/src/Popover.svelte

@@ -0,0 +1,23 @@
+<script lang="ts">
+  import type { BaseComponentProps } from "@json-render/svelte";
+  import type { ShadcnProps } from "./catalog.js";
+  import {
+    Popover,
+    PopoverContent,
+    PopoverTrigger,
+  } from "./ui/popover/index.js";
+  import { Button } from "./ui/button/index.js";
+
+  interface Props extends BaseComponentProps<ShadcnProps<"Popover">> {}
+
+  let { props }: Props = $props();
+</script>
+
+<Popover>
+  <PopoverTrigger>
+    {#snippet child({ props: triggerProps })}
+      <Button variant="outline" {...triggerProps}>{props.trigger}</Button>
+    {/snippet}
+  </PopoverTrigger>
+  <PopoverContent class="w-64 text-sm">{props.content}</PopoverContent>
+</Popover>

+ 19 - 0
packages/shadcn-svelte/src/Progress.svelte

@@ -0,0 +1,19 @@
+<script lang="ts">
+  import type { BaseComponentProps } from "@json-render/svelte";
+  import type { ShadcnProps } from "./catalog.js";
+  import { Progress } from "./ui/progress/index.js";
+
+  interface Props extends BaseComponentProps<ShadcnProps<"Progress">> {}
+
+  let { props }: Props = $props();
+
+  const max = $derived(props.max ?? 100);
+  const percent = $derived(Math.max(0, Math.min(100, (props.value / max) * 100)));
+</script>
+
+<div class="space-y-1">
+  {#if props.label}
+    <div class="text-sm text-muted-foreground">{props.label}</div>
+  {/if}
+  <Progress value={percent} />
+</div>

+ 63 - 0
packages/shadcn-svelte/src/Radio.svelte

@@ -0,0 +1,63 @@
+<script lang="ts">
+  import { untrack } from "svelte";
+  import { getBoundProp, getOptionalValidationContext } from "@json-render/svelte";
+  import type { BaseComponentProps } from "@json-render/svelte";
+  import type { ShadcnProps } from "./catalog.js";
+  import { createValidation } from "./helpers.js";
+  import { RadioGroup, RadioGroupItem } from "./ui/radio-group/index.js";
+  import { Label } from "./ui/label/index.js";
+
+  interface Props extends BaseComponentProps<ShadcnProps<"Radio">> {}
+
+  let { props, bindings, emit }: Props = $props();
+
+  let localValue = $state("");
+  let errors = $state<string[]>([]);
+
+  const validateOn = $derived((props.validateOn ?? "change") as "change" | "blur" | "submit");
+  const validationCtx = getOptionalValidationContext();
+  const validation = createValidation(
+    validationCtx,
+    untrack(() => bindings?.value),
+    untrack(() => props.checks ?? null),
+  );
+
+  $effect(() => {
+    const on = validateOn;
+    untrack(() => {
+      if (validation.hasValidation) validation.register(on);
+    });
+  });
+
+  const bound = getBoundProp<string>(
+    () => props.value ?? undefined,
+    () => bindings?.value,
+  );
+
+  const value = $derived(bound.current ?? localValue);
+
+  function choose(next: string) {
+    localValue = next;
+    bound.current = next;
+    if (validateOn === "change") errors = validation.run(validateOn);
+    emit("change");
+  }
+</script>
+
+<div class="space-y-2">
+  {#if props.label}
+    <span class="text-sm font-medium">{props.label}</span>
+  {/if}
+  <RadioGroup {value} onValueChange={choose} name={props.name}>
+    {#each props.options ?? [] as option, i}
+      {@const id = `${props.name}-${i}`}
+      <div class="flex items-center gap-2">
+        <RadioGroupItem value={option} {id} />
+        <Label for={id} class="text-sm cursor-pointer">{option}</Label>
+      </div>
+    {/each}
+  </RadioGroup>
+  {#if errors.length > 0}
+    <p class="text-sm text-destructive">{errors[0]}</p>
+  {/if}
+</div>

+ 63 - 0
packages/shadcn-svelte/src/Select.svelte

@@ -0,0 +1,63 @@
+<script lang="ts">
+  import { untrack } from "svelte";
+  import { getBoundProp, getOptionalValidationContext } from "@json-render/svelte";
+  import type { BaseComponentProps } from "@json-render/svelte";
+  import type { ShadcnProps } from "./catalog.js";
+  import * as Select from "./ui/select/index.js";
+  import { Label } from "./ui/label/index.js";
+  import { createValidation } from "./helpers.js";
+
+  interface Props extends BaseComponentProps<ShadcnProps<"Select">> {}
+
+  let { props, bindings, emit }: Props = $props();
+
+  let localValue = $state("");
+  let errors = $state<string[]>([]);
+
+  const validateOn = $derived((props.validateOn ?? "change") as "change" | "blur" | "submit");
+  const validationCtx = getOptionalValidationContext();
+  const validation = createValidation(
+    validationCtx,
+    untrack(() => bindings?.value),
+    untrack(() => props.checks ?? null),
+  );
+
+  $effect(() => {
+    const on = validateOn;
+    untrack(() => {
+      if (validation.hasValidation) validation.register(on);
+    });
+  });
+
+  const bound = getBoundProp<string>(
+    () => props.value ?? undefined,
+    () => bindings?.value,
+  );
+
+  const value = $derived(bound.current ?? localValue);
+
+  function handleValueChange(next: string | undefined) {
+    if (!next) return;
+    localValue = next;
+    bound.current = next;
+    if (validateOn === "change") errors = validation.run(validateOn);
+    emit("change");
+  }
+</script>
+
+<div class="space-y-2">
+  <Label>{props.label}</Label>
+  <Select.Root type="single" {value} onValueChange={handleValueChange}>
+    <Select.Trigger>
+      {value || props.placeholder || "Select..."}
+    </Select.Trigger>
+    <Select.Content>
+      {#each props.options ?? [] as option, i}
+        <Select.Item value={option || `option-${i}`}>{option}</Select.Item>
+      {/each}
+    </Select.Content>
+  </Select.Root>
+  {#if errors.length > 0}
+    <p class="text-sm text-destructive">{errors[0]}</p>
+  {/if}
+</div>

+ 14 - 0
packages/shadcn-svelte/src/Separator.svelte

@@ -0,0 +1,14 @@
+<script lang="ts">
+  import type { BaseComponentProps } from "@json-render/svelte";
+  import type { ShadcnProps } from "./catalog.js";
+  import { Separator } from "./ui/separator/index.js";
+
+  interface Props extends BaseComponentProps<ShadcnProps<"Separator">> {}
+
+  let { props }: Props = $props();
+</script>
+
+<Separator
+  orientation={props.orientation ?? "horizontal"}
+  class={props.orientation === "vertical" ? "h-full mx-2" : "my-3"}
+/>

+ 13 - 0
packages/shadcn-svelte/src/Skeleton.svelte

@@ -0,0 +1,13 @@
+<script lang="ts">
+  import type { BaseComponentProps } from "@json-render/svelte";
+  import type { ShadcnProps } from "./catalog.js";
+
+  interface Props extends BaseComponentProps<ShadcnProps<"Skeleton">> {}
+
+  let { props }: Props = $props();
+</script>
+
+<div
+  class={`animate-pulse bg-muted ${props.rounded ? "rounded-full" : "rounded-md"}`}
+  style={`width:${props.width ?? "100%"};height:${props.height ?? "1rem"};`}
+></div>

+ 44 - 0
packages/shadcn-svelte/src/Slider.svelte

@@ -0,0 +1,44 @@
+<script lang="ts">
+  import { untrack } from "svelte";
+  import { getBoundProp } from "@json-render/svelte";
+  import type { BaseComponentProps } from "@json-render/svelte";
+  import type { ShadcnProps } from "./catalog.js";
+  import { Slider } from "./ui/slider/index.js";
+
+  interface Props extends BaseComponentProps<ShadcnProps<"Slider">> {}
+
+  let { props, bindings, emit }: Props = $props();
+
+  let localValue = $state(untrack(() => props.value ?? props.min ?? 0));
+
+  const bound = getBoundProp<number>(
+    () => props.value ?? undefined,
+    () => bindings?.value,
+  );
+
+  const value = $derived(bound.current ?? localValue);
+
+  function handleChange(next: number) {
+    localValue = next;
+    bound.current = next;
+    emit("change");
+  }
+</script>
+
+<div class="space-y-2">
+  {#if props.label}
+    <div class="flex items-center justify-between text-sm">
+      <span>{props.label}</span>
+      <span class="text-muted-foreground">{value}</span>
+    </div>
+  {/if}
+  <Slider
+    type="single"
+    value={value}
+    min={props.min ?? 0}
+    max={props.max ?? 100}
+    step={props.step ?? 1}
+    onValueChange={handleChange}
+    class="w-full"
+  />
+</div>

+ 19 - 0
packages/shadcn-svelte/src/Spinner.svelte

@@ -0,0 +1,19 @@
+<script lang="ts">
+  import type { BaseComponentProps } from "@json-render/svelte";
+  import type { ShadcnProps } from "./catalog.js";
+
+  interface Props extends BaseComponentProps<ShadcnProps<"Spinner">> {}
+
+  let { props }: Props = $props();
+
+  const sizeClass = $derived(
+    props.size === "sm" ? "size-4" : props.size === "lg" ? "size-8" : "size-6",
+  );
+</script>
+
+<div class="inline-flex items-center gap-2 text-muted-foreground">
+  <div class={`animate-spin rounded-full border-2 border-current border-t-transparent ${sizeClass}`}></div>
+  {#if props.label}
+    <span class="text-sm">{props.label}</span>
+  {/if}
+</div>

+ 39 - 0
packages/shadcn-svelte/src/Stack.svelte

@@ -0,0 +1,39 @@
+<script lang="ts">
+  import type { Snippet } from "svelte";
+  import type { BaseComponentProps } from "@json-render/svelte";
+  import type { ShadcnProps } from "./catalog.js";
+
+  interface Props extends BaseComponentProps<ShadcnProps<"Stack">> {
+    children?: Snippet;
+  }
+
+  let { props, children }: Props = $props();
+
+  const gapMap: Record<string, string> = {
+    none: "gap-0",
+    sm: "gap-2",
+    md: "gap-3",
+    lg: "gap-4",
+  };
+
+  const alignMap: Record<string, string> = {
+    start: "items-start",
+    center: "items-center",
+    end: "items-end",
+    stretch: "items-stretch",
+  };
+
+  const justifyMap: Record<string, string> = {
+    start: "",
+    center: "justify-center",
+    end: "justify-end",
+    between: "justify-between",
+    around: "justify-around",
+  };
+</script>
+
+<div
+  class={`flex ${props.direction === "horizontal" ? "flex-row flex-wrap" : "flex-col"} ${gapMap[props.gap ?? "md"] ?? "gap-3"} ${alignMap[props.align ?? "start"] ?? "items-start"} ${justifyMap[props.justify ?? "start"] ?? ""}`}
+>
+  {@render children?.()}
+</div>

+ 57 - 0
packages/shadcn-svelte/src/Switch.svelte

@@ -0,0 +1,57 @@
+<script lang="ts">
+  import { untrack } from "svelte";
+  import { getBoundProp, getOptionalValidationContext } from "@json-render/svelte";
+  import type { BaseComponentProps } from "@json-render/svelte";
+  import type { ShadcnProps } from "./catalog.js";
+  import { createValidation } from "./helpers.js";
+  import { Switch } from "./ui/switch/index.js";
+  import { Label } from "./ui/label/index.js";
+
+  interface Props extends BaseComponentProps<ShadcnProps<"Switch">> {}
+
+  let { props, bindings, emit }: Props = $props();
+
+  let localChecked = $state(untrack(() => Boolean(props.checked)));
+  let errors = $state<string[]>([]);
+
+  const validateOn = $derived((props.validateOn ?? "change") as "change" | "blur" | "submit");
+  const validationCtx = getOptionalValidationContext();
+  const validation = createValidation(
+    validationCtx,
+    untrack(() => bindings?.checked),
+    untrack(() => props.checks ?? null),
+  );
+
+  $effect(() => {
+    const on = validateOn;
+    untrack(() => {
+      if (validation.hasValidation) validation.register(on);
+    });
+  });
+
+  const bound = getBoundProp<boolean>(
+    () => props.checked ?? undefined,
+    () => bindings?.checked,
+  );
+
+  const checked = $derived(bound.current ?? localChecked);
+
+  function handleChange(value: boolean) {
+    localChecked = value;
+    bound.current = value;
+    if (validateOn === "change") errors = validation.run(validateOn);
+    emit("change");
+  }
+</script>
+
+<div class="space-y-1">
+  <div class="flex items-center gap-2">
+    <Switch id={props.name} {checked} onCheckedChange={handleChange} />
+    {#if props.label}
+      <Label for={props.name} class="text-sm cursor-pointer">{props.label}</Label>
+    {/if}
+  </div>
+  {#if errors.length > 0}
+    <p class="text-sm text-destructive">{errors[0]}</p>
+  {/if}
+</div>

+ 36 - 0
packages/shadcn-svelte/src/Table.svelte

@@ -0,0 +1,36 @@
+<script lang="ts">
+  import type { BaseComponentProps } from "@json-render/svelte";
+  import type { ShadcnProps } from "./catalog.js";
+  import * as Table from "./ui/table/index.js";
+
+  interface Props extends BaseComponentProps<ShadcnProps<"Table">> {}
+
+  let { props }: Props = $props();
+
+  const columns = $derived(props.columns ?? []);
+  const rows = $derived((props.rows ?? []).map((row) => row.map((cell) => String(cell))));
+</script>
+
+<div class="rounded-md border overflow-hidden">
+  <Table.Root>
+    {#if props.caption}
+      <Table.Caption>{props.caption}</Table.Caption>
+    {/if}
+    <Table.Header>
+      <Table.Row>
+        {#each columns as col}
+          <Table.Head>{col}</Table.Head>
+        {/each}
+      </Table.Row>
+    </Table.Header>
+    <Table.Body>
+      {#each rows as row}
+        <Table.Row>
+          {#each row as cell}
+            <Table.Cell>{cell}</Table.Cell>
+          {/each}
+        </Table.Row>
+      {/each}
+    </Table.Body>
+  </Table.Root>
+</div>

+ 49 - 0
packages/shadcn-svelte/src/Tabs.svelte

@@ -0,0 +1,49 @@
+<script lang="ts">
+  import { untrack } from "svelte";
+  import type { Snippet } from "svelte";
+  import { getBoundProp } from "@json-render/svelte";
+  import type { BaseComponentProps } from "@json-render/svelte";
+  import type { ShadcnProps } from "./catalog.js";
+  import * as Tabs from "./ui/tabs/index.js";
+
+  interface Props extends BaseComponentProps<ShadcnProps<"Tabs">> {
+    children?: Snippet;
+  }
+
+  let { props, children, bindings, emit }: Props = $props();
+
+  const tabs = $derived(props.tabs ?? []);
+  let localValue = $state(untrack(() => props.defaultValue ?? tabs[0]?.value ?? ""));
+  let activeValue = $state(untrack(() => localValue));
+  let previousValue = $state(untrack(() => activeValue));
+
+  const bound = getBoundProp<string>(
+    () => props.value ?? undefined,
+    () => bindings?.value,
+  );
+
+  $effect(() => {
+    const current = bound.current ?? localValue ?? tabs[0]?.value ?? "";
+    if (current !== activeValue) {
+      activeValue = current;
+      previousValue = current;
+    }
+  });
+
+  $effect(() => {
+    if (activeValue === previousValue) return;
+    previousValue = activeValue;
+    localValue = activeValue;
+    bound.current = activeValue;
+    emit("change");
+  });
+</script>
+
+<Tabs.Root bind:value={activeValue}>
+  <Tabs.List>
+    {#each tabs as tab}
+      <Tabs.Trigger value={tab.value}>{tab.label}</Tabs.Trigger>
+    {/each}
+  </Tabs.List>
+  {@render children?.()}
+</Tabs.Root>

+ 22 - 0
packages/shadcn-svelte/src/Text.svelte

@@ -0,0 +1,22 @@
+<script lang="ts">
+  import type { BaseComponentProps } from "@json-render/svelte";
+  import type { ShadcnProps } from "./catalog.js";
+
+  interface Props extends BaseComponentProps<ShadcnProps<"Text">> {}
+
+  let { props }: Props = $props();
+
+  const className = $derived(
+    props.variant === "caption"
+      ? "text-xs text-muted-foreground"
+      : props.variant === "muted"
+        ? "text-sm text-muted-foreground"
+        : props.variant === "lead"
+          ? "text-lg text-foreground"
+          : props.variant === "code"
+            ? "rounded bg-muted px-1 py-0.5 font-mono text-sm"
+            : "text-sm text-foreground",
+  );
+</script>
+
+<p class={className}>{props.text}</p>

+ 65 - 0
packages/shadcn-svelte/src/Textarea.svelte

@@ -0,0 +1,65 @@
+<script lang="ts">
+  import { untrack } from "svelte";
+  import { getBoundProp, getOptionalValidationContext } from "@json-render/svelte";
+  import type { BaseComponentProps } from "@json-render/svelte";
+  import type { ShadcnProps } from "./catalog.js";
+  import { Label } from "./ui/label/index.js";
+  import { createValidation } from "./helpers.js";
+
+  interface Props extends BaseComponentProps<ShadcnProps<"Textarea">> {}
+
+  let { props, bindings }: Props = $props();
+
+  let localValue = $state("");
+  let errors = $state<string[]>([]);
+
+  const validateOn = $derived((props.validateOn ?? "blur") as "change" | "blur" | "submit");
+  const validationCtx = getOptionalValidationContext();
+  const validation = createValidation(
+    validationCtx,
+    untrack(() => bindings?.value),
+    untrack(() => props.checks ?? null),
+  );
+
+  $effect(() => {
+    const on = validateOn;
+    untrack(() => {
+      if (validation.hasValidation) validation.register(on);
+    });
+  });
+
+  const bound = getBoundProp<string>(
+    () => props.value ?? undefined,
+    () => bindings?.value,
+  );
+
+  const value = $derived(bound.current ?? localValue);
+
+  function handleInput(event: Event) {
+    const next = (event.target as HTMLTextAreaElement).value;
+    localValue = next;
+    bound.current = next;
+    if (validateOn === "change") errors = validation.run(validateOn);
+  }
+
+  function handleBlur() {
+    if (validateOn === "blur") errors = validation.run(validateOn);
+  }
+</script>
+
+<div class="space-y-2">
+  <Label for={props.name}>{props.label}</Label>
+  <textarea
+    id={props.name}
+    name={props.name}
+    class="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex min-h-[80px] w-full rounded-md border px-3 py-2 text-sm outline-none focus-visible:ring-2"
+    placeholder={props.placeholder ?? ""}
+    rows={props.rows ?? 3}
+    value={value}
+    oninput={handleInput}
+    onblur={handleBlur}
+  ></textarea>
+  {#if errors.length > 0}
+    <p class="text-sm text-destructive">{errors[0]}</p>
+  {/if}
+</div>

+ 28 - 0
packages/shadcn-svelte/src/Toggle.svelte

@@ -0,0 +1,28 @@
+<script lang="ts">
+  import { untrack } from "svelte";
+  import { getBoundProp } from "@json-render/svelte";
+  import type { BaseComponentProps } from "@json-render/svelte";
+  import type { ShadcnProps } from "./catalog.js";
+  import { Toggle } from "./ui/toggle/index.js";
+
+  interface Props extends BaseComponentProps<ShadcnProps<"Toggle">> {}
+
+  let { props, bindings, emit }: Props = $props();
+
+  let localPressed = $state(untrack(() => props.pressed ?? false));
+
+  const bound = getBoundProp<boolean>(
+    () => props.pressed ?? undefined,
+    () => bindings?.pressed,
+  );
+
+  const pressed = $derived(bound.current ?? localPressed);
+
+  function handleChange(next: boolean) {
+    localPressed = next;
+    bound.current = next;
+    emit("change");
+  }
+</script>
+
+<Toggle {pressed} onPressedChange={handleChange}>{props.label}</Toggle>

+ 52 - 0
packages/shadcn-svelte/src/ToggleGroup.svelte

@@ -0,0 +1,52 @@
+<script lang="ts">
+  import { getBoundProp } from "@json-render/svelte";
+  import type { BaseComponentProps } from "@json-render/svelte";
+  import type { ShadcnProps } from "./catalog.js";
+  import { ToggleGroup, ToggleGroupItem } from "./ui/toggle-group/index.js";
+
+  interface Props extends BaseComponentProps<ShadcnProps<"ToggleGroup">> {}
+
+  let { props, bindings, emit }: Props = $props();
+
+  let localValue = $state("");
+
+  const bound = getBoundProp<string>(
+    () => props.value ?? undefined,
+    () => bindings?.value,
+  );
+
+  const value = $derived(bound.current ?? localValue);
+  const isMultiple = $derived((props.type ?? "single") === "multiple");
+  const selectedValues = $derived(value ? value.split(",").filter(Boolean) : []);
+
+  function handleChange(next: string | string[] | undefined) {
+    const serialized = Array.isArray(next) ? next.join(",") : (next ?? "");
+    localValue = serialized;
+    bound.current = serialized;
+    emit("change");
+  }
+</script>
+
+{#if isMultiple}
+  <ToggleGroup
+    type="multiple"
+    value={selectedValues}
+    onValueChange={handleChange}
+    variant="outline"
+  >
+    {#each props.items ?? [] as item}
+      <ToggleGroupItem value={item.value}>{item.label}</ToggleGroupItem>
+    {/each}
+  </ToggleGroup>
+{:else}
+  <ToggleGroup
+    type="single"
+    value={selectedValues[0] ?? ""}
+    onValueChange={handleChange}
+    variant="outline"
+  >
+    {#each props.items ?? [] as item}
+      <ToggleGroupItem value={item.value}>{item.label}</ToggleGroupItem>
+    {/each}
+  </ToggleGroup>
+{/if}

+ 23 - 0
packages/shadcn-svelte/src/Tooltip.svelte

@@ -0,0 +1,23 @@
+<script lang="ts">
+  import type { BaseComponentProps } from "@json-render/svelte";
+  import type { ShadcnProps } from "./catalog.js";
+  import {
+    Tooltip,
+    TooltipContent,
+    TooltipProvider,
+    TooltipTrigger,
+  } from "./ui/tooltip/index.js";
+
+  interface Props extends BaseComponentProps<ShadcnProps<"Tooltip">> {}
+
+  let { props }: Props = $props();
+</script>
+
+<TooltipProvider>
+  <Tooltip>
+    <TooltipTrigger class="cursor-help text-sm underline decoration-dotted">
+      {props.text}
+    </TooltipTrigger>
+    <TooltipContent>{props.content}</TooltipContent>
+  </Tooltip>
+</TooltipProvider>

+ 69 - 0
packages/shadcn-svelte/src/app.css

@@ -0,0 +1,69 @@
+@import "tw-animate-css";
+
+@layer base {
+	* {
+		@apply border-border outline-ring/50;
+	}
+}
+
+@custom-variant data-open {
+	&:where([data-state="open"]), &:where([data-open]:not([data-open="false"])) {
+		@slot;
+	}
+}
+
+@custom-variant data-closed {
+	&:where([data-state="closed"]), &:where([data-closed]:not([data-closed="false"])) {
+		@slot;
+	}
+}
+
+@custom-variant data-checked {
+	&:where([data-state="checked"]), &:where([data-checked]:not([data-checked="false"])) {
+		@slot;
+	}
+}
+
+@custom-variant data-unchecked {
+	&:where([data-state="unchecked"]), &:where([data-unchecked]:not([data-unchecked="false"])) {
+		@slot;
+	}
+}
+
+@custom-variant data-selected {
+	&:where([data-selected]) {
+		@slot;
+	}
+}
+
+@custom-variant data-disabled {
+	&:where([data-disabled="true"]), &:where([data-disabled]:not([data-disabled="false"])) {
+		@slot;
+	}
+}
+
+@custom-variant data-active {
+	&:where([data-state="active"]), &:where([data-active]:not([data-active="false"])) {
+		@slot;
+	}
+}
+
+@custom-variant data-horizontal {
+	&:where([data-orientation="horizontal"]) {
+		@slot;
+	}
+}
+
+@custom-variant data-vertical {
+	&:where([data-orientation="vertical"]) {
+		@slot;
+	}
+}
+
+@utility no-scrollbar {
+	-ms-overflow-style: none;
+	scrollbar-width: none;
+	&::-webkit-scrollbar {
+		display: none;
+	}
+}

+ 506 - 0
packages/shadcn-svelte/src/catalog.ts

@@ -0,0 +1,506 @@
+import { z } from "zod";
+
+// =============================================================================
+// Shared validation schemas used across form components
+// =============================================================================
+
+const validationCheckSchema = z
+  .array(
+    z.object({
+      type: z.string(),
+      message: z.string(),
+      args: z.record(z.string(), z.unknown()).optional(),
+    }),
+  )
+  .nullable();
+
+const validateOnSchema = z.enum(["change", "blur", "submit"]).nullable();
+
+// =============================================================================
+// shadcn/ui Component Definitions
+// =============================================================================
+
+/**
+ * shadcn/ui component definitions for json-render catalogs.
+ *
+ * These can be used directly or extended with custom components.
+ * All components are built using Radix UI primitives + Tailwind CSS.
+ */
+export const shadcnComponentDefinitions = {
+  // ==========================================================================
+  // Layout Components
+  // ==========================================================================
+
+  Card: {
+    props: z.object({
+      title: z.string().nullable(),
+      description: z.string().nullable(),
+      maxWidth: z.enum(["sm", "md", "lg", "full"]).nullable(),
+      centered: z.boolean().nullable(),
+    }),
+    slots: ["default"],
+    description:
+      "Container card for content sections. Use for forms/content boxes, NOT for page headers.",
+    example: { title: "Overview", description: "Your account summary" },
+  },
+
+  Stack: {
+    props: z.object({
+      direction: z.enum(["horizontal", "vertical"]).nullable(),
+      gap: z.enum(["none", "sm", "md", "lg"]).nullable(),
+      align: z.enum(["start", "center", "end", "stretch"]).nullable(),
+      justify: z
+        .enum(["start", "center", "end", "between", "around"])
+        .nullable(),
+    }),
+    slots: ["default"],
+    description: "Flex container for layouts",
+    example: { direction: "vertical", gap: "md" },
+  },
+
+  Grid: {
+    props: z.object({
+      columns: z.number().nullable(),
+      gap: z.enum(["sm", "md", "lg"]).nullable(),
+    }),
+    slots: ["default"],
+    description: "Grid layout (1-6 columns)",
+    example: { columns: 3, gap: "md" },
+  },
+
+  Separator: {
+    props: z.object({
+      orientation: z.enum(["horizontal", "vertical"]).nullable(),
+    }),
+    description: "Visual separator line",
+  },
+
+  Tabs: {
+    props: z.object({
+      tabs: z.array(
+        z.object({
+          label: z.string(),
+          value: z.string(),
+        }),
+      ),
+      defaultValue: z.string().nullable(),
+      value: z.string().nullable(),
+    }),
+    slots: ["default"],
+    events: ["change"],
+    description:
+      "Tab navigation. Use { $bindState } on value for active tab binding.",
+  },
+
+  Accordion: {
+    props: z.object({
+      items: z.array(
+        z.object({
+          title: z.string(),
+          content: z.string(),
+        }),
+      ),
+      type: z.enum(["single", "multiple"]).nullable(),
+    }),
+    description:
+      "Collapsible sections. Items as [{title, content}]. Type 'single' (default) or 'multiple'.",
+  },
+
+  Collapsible: {
+    props: z.object({
+      title: z.string(),
+      defaultOpen: z.boolean().nullable(),
+    }),
+    slots: ["default"],
+    description: "Collapsible section with trigger. Children render inside.",
+  },
+
+  Dialog: {
+    props: z.object({
+      title: z.string(),
+      description: z.string().nullable(),
+      openPath: z.string(),
+    }),
+    slots: ["default"],
+    description:
+      "Modal dialog. Set openPath to a boolean state path. Use setState to toggle.",
+  },
+
+  Drawer: {
+    props: z.object({
+      title: z.string(),
+      description: z.string().nullable(),
+      openPath: z.string(),
+    }),
+    slots: ["default"],
+    description:
+      "Bottom sheet drawer. Set openPath to a boolean state path. Use setState to toggle.",
+  },
+
+  Carousel: {
+    props: z.object({
+      items: z.array(
+        z.object({
+          title: z.string().nullable(),
+          description: z.string().nullable(),
+        }),
+      ),
+    }),
+    description: "Horizontally scrollable carousel of cards.",
+  },
+
+  // ==========================================================================
+  // Data Display Components
+  // ==========================================================================
+
+  Table: {
+    props: z.object({
+      columns: z.array(z.string()),
+      rows: z.array(z.array(z.string())),
+      caption: z.string().nullable(),
+    }),
+    description:
+      'Data table. columns: header labels. rows: 2D array of cell strings, e.g. [["Alice","admin"],["Bob","user"]].',
+    example: {
+      columns: ["Name", "Role"],
+      rows: [
+        ["Alice", "Admin"],
+        ["Bob", "User"],
+      ],
+    },
+  },
+
+  Heading: {
+    props: z.object({
+      text: z.string(),
+      level: z.enum(["h1", "h2", "h3", "h4"]).nullable(),
+    }),
+    description: "Heading text (h1-h4)",
+    example: { text: "Welcome", level: "h1" },
+  },
+
+  Text: {
+    props: z.object({
+      text: z.string(),
+      variant: z.enum(["body", "caption", "muted", "lead", "code"]).nullable(),
+    }),
+    description: "Paragraph text",
+    example: { text: "Hello, world!" },
+  },
+
+  Image: {
+    props: z.object({
+      src: z.string().nullable(),
+      alt: z.string(),
+      width: z.number().nullable(),
+      height: z.number().nullable(),
+    }),
+    description:
+      "Image component. Renders an img tag when src is provided, otherwise a placeholder.",
+  },
+
+  Avatar: {
+    props: z.object({
+      src: z.string().nullable(),
+      name: z.string(),
+      size: z.enum(["sm", "md", "lg"]).nullable(),
+    }),
+    description: "User avatar with fallback initials",
+    example: { name: "Jane Doe", size: "md" },
+  },
+
+  Badge: {
+    props: z.object({
+      text: z.string(),
+      variant: z
+        .enum(["default", "secondary", "destructive", "outline"])
+        .nullable(),
+    }),
+    description: "Status badge",
+    example: { text: "Active", variant: "default" },
+  },
+
+  Alert: {
+    props: z.object({
+      title: z.string(),
+      message: z.string().nullable(),
+      type: z.enum(["info", "success", "warning", "error"]).nullable(),
+    }),
+    description: "Alert banner",
+    example: {
+      title: "Note",
+      message: "Your changes have been saved.",
+      type: "success",
+    },
+  },
+
+  Progress: {
+    props: z.object({
+      value: z.number(),
+      max: z.number().nullable(),
+      label: z.string().nullable(),
+    }),
+    description: "Progress bar (value 0-100)",
+    example: { value: 65, max: 100, label: "Upload progress" },
+  },
+
+  Skeleton: {
+    props: z.object({
+      width: z.string().nullable(),
+      height: z.string().nullable(),
+      rounded: z.boolean().nullable(),
+    }),
+    description: "Loading placeholder skeleton",
+  },
+
+  Spinner: {
+    props: z.object({
+      size: z.enum(["sm", "md", "lg"]).nullable(),
+      label: z.string().nullable(),
+    }),
+    description: "Loading spinner indicator",
+  },
+
+  Tooltip: {
+    props: z.object({
+      content: z.string(),
+      text: z.string(),
+    }),
+    description: "Hover tooltip. Shows content on hover over text.",
+  },
+
+  Popover: {
+    props: z.object({
+      trigger: z.string(),
+      content: z.string(),
+    }),
+    description: "Popover that appears on click of trigger.",
+  },
+
+  // ==========================================================================
+  // Form Input Components
+  // ==========================================================================
+
+  Input: {
+    props: z.object({
+      label: z.string(),
+      name: z.string(),
+      type: z.enum(["text", "email", "password", "number"]).nullable(),
+      placeholder: z.string().nullable(),
+      value: z.string().nullable(),
+      checks: validationCheckSchema,
+      validateOn: validateOnSchema,
+    }),
+    events: ["submit", "focus", "blur"],
+    description:
+      "Text input field. Use { $bindState } on value for two-way binding. Use checks for validation (e.g. required, email, minLength). validateOn controls timing (default: blur).",
+    example: {
+      label: "Email",
+      name: "email",
+      type: "email",
+      placeholder: "you@example.com",
+    },
+  },
+
+  Textarea: {
+    props: z.object({
+      label: z.string(),
+      name: z.string(),
+      placeholder: z.string().nullable(),
+      rows: z.number().nullable(),
+      value: z.string().nullable(),
+      checks: validationCheckSchema,
+      validateOn: validateOnSchema,
+    }),
+    description:
+      "Multi-line text input. Use { $bindState } on value for binding. Use checks for validation. validateOn controls timing (default: blur).",
+  },
+
+  Select: {
+    props: z.object({
+      label: z.string(),
+      name: z.string(),
+      options: z.array(z.string()),
+      placeholder: z.string().nullable(),
+      value: z.string().nullable(),
+      checks: validationCheckSchema,
+      validateOn: validateOnSchema,
+    }),
+    events: ["change"],
+    description:
+      "Dropdown select input. Use { $bindState } on value for binding. Use checks for validation. validateOn controls timing (default: change).",
+  },
+
+  Checkbox: {
+    props: z.object({
+      label: z.string(),
+      name: z.string(),
+      checked: z.boolean().nullable(),
+      checks: validationCheckSchema,
+      validateOn: validateOnSchema,
+    }),
+    events: ["change"],
+    description:
+      "Checkbox input. Use { $bindState } on checked for binding. Use checks for validation. validateOn controls timing (default: change).",
+  },
+
+  Radio: {
+    props: z.object({
+      label: z.string(),
+      name: z.string(),
+      options: z.array(z.string()),
+      value: z.string().nullable(),
+      checks: validationCheckSchema,
+      validateOn: validateOnSchema,
+    }),
+    events: ["change"],
+    description:
+      "Radio button group. Use { $bindState } on value for binding. Use checks for validation. validateOn controls timing (default: change).",
+  },
+
+  Switch: {
+    props: z.object({
+      label: z.string(),
+      name: z.string(),
+      checked: z.boolean().nullable(),
+      checks: validationCheckSchema,
+      validateOn: validateOnSchema,
+    }),
+    events: ["change"],
+    description:
+      "Toggle switch. Use { $bindState } on checked for binding. Use checks for validation. validateOn controls timing (default: change).",
+  },
+
+  Slider: {
+    props: z.object({
+      label: z.string().nullable(),
+      min: z.number().nullable(),
+      max: z.number().nullable(),
+      step: z.number().nullable(),
+      value: z.number().nullable(),
+    }),
+    events: ["change"],
+    description: "Range slider input. Use { $bindState } on value for binding.",
+  },
+
+  // ==========================================================================
+  // Action Components
+  // ==========================================================================
+
+  Button: {
+    props: z.object({
+      label: z.string(),
+      variant: z.enum(["primary", "secondary", "danger"]).nullable(),
+      disabled: z.boolean().nullable(),
+    }),
+    events: ["press"],
+    description: "Clickable button. Bind on.press for handler.",
+    example: { label: "Submit", variant: "primary" },
+  },
+
+  Link: {
+    props: z.object({
+      label: z.string(),
+      href: z.string(),
+    }),
+    events: ["press"],
+    description: "Anchor link. Bind on.press for click handler.",
+  },
+
+  DropdownMenu: {
+    props: z.object({
+      label: z.string(),
+      items: z.array(
+        z.object({
+          label: z.string(),
+          value: z.string(),
+        }),
+      ),
+      value: z.string().nullable(),
+    }),
+    events: ["select"],
+    description:
+      "Dropdown menu with trigger button and selectable items. Use { $bindState } on value for selected item binding.",
+  },
+
+  Toggle: {
+    props: z.object({
+      label: z.string(),
+      pressed: z.boolean().nullable(),
+      variant: z.enum(["default", "outline"]).nullable(),
+    }),
+    events: ["change"],
+    description:
+      "Toggle button. Use { $bindState } on pressed for state binding.",
+  },
+
+  ToggleGroup: {
+    props: z.object({
+      items: z.array(
+        z.object({
+          label: z.string(),
+          value: z.string(),
+        }),
+      ),
+      type: z.enum(["single", "multiple"]).nullable(),
+      value: z.string().nullable(),
+    }),
+    events: ["change"],
+    description:
+      "Group of toggle buttons. Type 'single' (default) or 'multiple'. Use { $bindState } on value.",
+  },
+
+  ButtonGroup: {
+    props: z.object({
+      buttons: z.array(
+        z.object({
+          label: z.string(),
+          value: z.string(),
+        }),
+      ),
+      selected: z.string().nullable(),
+    }),
+    events: ["change"],
+    description:
+      "Segmented button group. Use { $bindState } on selected for selected value.",
+  },
+
+  Pagination: {
+    props: z.object({
+      totalPages: z.number(),
+      page: z.number().nullable(),
+    }),
+    events: ["change"],
+    description:
+      "Page navigation. Use { $bindState } on page for current page number.",
+  },
+};
+
+// =============================================================================
+// Types
+// =============================================================================
+
+/**
+ * Type for a component definition
+ */
+export type ComponentDefinition = {
+  props: z.ZodType;
+  slots?: string[];
+  events?: string[];
+  description: string;
+  example?: Record<string, unknown>;
+};
+
+/**
+ * Infer the props type for a shadcn component by name.
+ * Derives the TypeScript type directly from the Zod schema,
+ * so component implementations stay in sync with catalog definitions.
+ *
+ * @example
+ * ```ts
+ * type CardProps = ShadcnProps<"Card">;
+ * // { title: string | null; description: string | null; ... }
+ * ```
+ */
+export type ShadcnProps<K extends keyof typeof shadcnComponentDefinitions> =
+  z.output<(typeof shadcnComponentDefinitions)[K]["props"]>;

+ 77 - 0
packages/shadcn-svelte/src/components.ts

@@ -0,0 +1,77 @@
+import type { Component } from "svelte";
+import Card from "./Card.svelte";
+import Stack from "./Stack.svelte";
+import Grid from "./Grid.svelte";
+import Separator from "./Separator.svelte";
+import Tabs from "./Tabs.svelte";
+import Accordion from "./Accordion.svelte";
+import Collapsible from "./Collapsible.svelte";
+import Dialog from "./Dialog.svelte";
+import Drawer from "./Drawer.svelte";
+import Carousel from "./Carousel.svelte";
+import Table from "./Table.svelte";
+import Heading from "./Heading.svelte";
+import Text from "./Text.svelte";
+import Image from "./Image.svelte";
+import Avatar from "./Avatar.svelte";
+import Badge from "./Badge.svelte";
+import Alert from "./Alert.svelte";
+import Progress from "./Progress.svelte";
+import Skeleton from "./Skeleton.svelte";
+import Spinner from "./Spinner.svelte";
+import Tooltip from "./Tooltip.svelte";
+import Popover from "./Popover.svelte";
+import Input from "./Input.svelte";
+import Textarea from "./Textarea.svelte";
+import Select from "./Select.svelte";
+import Checkbox from "./Checkbox.svelte";
+import Radio from "./Radio.svelte";
+import Switch from "./Switch.svelte";
+import Slider from "./Slider.svelte";
+import Button from "./Button.svelte";
+import Link from "./Link.svelte";
+import DropdownMenu from "./DropdownMenu.svelte";
+import Toggle from "./Toggle.svelte";
+import ToggleGroup from "./ToggleGroup.svelte";
+import ButtonGroup from "./ButtonGroup.svelte";
+import Pagination from "./Pagination.svelte";
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export const shadcnComponents: Record<string, Component<any>> = {
+  Card,
+  Stack,
+  Grid,
+  Separator,
+  Tabs,
+  Accordion,
+  Collapsible,
+  Dialog,
+  Drawer,
+  Carousel,
+  Table,
+  Heading,
+  Text,
+  Image,
+  Avatar,
+  Badge,
+  Alert,
+  Progress,
+  Skeleton,
+  Spinner,
+  Tooltip,
+  Popover,
+  Input,
+  Textarea,
+  Select,
+  Checkbox,
+  Radio,
+  Switch,
+  Slider,
+  Button,
+  Link,
+  DropdownMenu,
+  Toggle,
+  ToggleGroup,
+  ButtonGroup,
+  Pagination,
+};

+ 62 - 0
packages/shadcn-svelte/src/helpers.ts

@@ -0,0 +1,62 @@
+import { getOptionalValidationContext } from "@json-render/svelte";
+
+type ValidateOn = "change" | "blur" | "submit";
+
+type ValidationCheck = {
+  type: string;
+  message: string;
+  args?: Record<string, unknown>;
+};
+
+export type ValidationCtx = ReturnType<typeof getOptionalValidationContext>;
+
+export function getPaginationRange(
+  current: number,
+  total: number,
+): Array<number | "ellipsis"> {
+  if (total <= 7) {
+    return Array.from({ length: total }, (_, i) => i + 1);
+  }
+
+  const pages: Array<number | "ellipsis"> = [1];
+  if (current > 3) pages.push("ellipsis");
+
+  const start = Math.max(2, current - 1);
+  const end = Math.min(total - 1, current + 1);
+  for (let i = start; i <= end; i += 1) pages.push(i);
+
+  if (current < total - 2) pages.push("ellipsis");
+  pages.push(total);
+  return pages;
+}
+
+export function createValidation(
+  validation: ValidationCtx,
+  path?: string,
+  checks?: ValidationCheck[] | null,
+) {
+  const config = checks && checks.length > 0 ? { checks } : null;
+
+  function register(validateOn: ValidateOn) {
+    if (!validation || !path || !config) return;
+    validation.registerField(path, {
+      checks: config.checks,
+      validateOn,
+    });
+  }
+
+  function run(validateOn: ValidateOn): string[] {
+    if (!validation || !path || !config) return [];
+    const result = validation.validate(path, {
+      checks: config.checks,
+      validateOn,
+    });
+    return result.errors;
+  }
+
+  return {
+    hasValidation: !!(validation && path && config),
+    register,
+    run,
+  };
+}

+ 7 - 0
packages/shadcn-svelte/src/index.ts

@@ -0,0 +1,7 @@
+export { shadcnComponents } from "./components.js";
+
+export {
+  shadcnComponentDefinitions,
+  type ComponentDefinition,
+  type ShadcnProps,
+} from "./catalog.js";

+ 17 - 0
packages/shadcn-svelte/src/lib/utils.ts

@@ -0,0 +1,17 @@
+import { clsx, type ClassValue } from "clsx";
+import { twMerge } from "tailwind-merge";
+
+export function cn(...inputs: ClassValue[]) {
+  return twMerge(clsx(inputs));
+}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export type WithoutChild<T> = T extends { child?: any } ? Omit<T, "child"> : T;
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export type WithoutChildren<T> = T extends { children?: any }
+  ? Omit<T, "children">
+  : T;
+export type WithoutChildrenOrChild<T> = WithoutChildren<WithoutChild<T>>;
+export type WithElementRef<T, U extends HTMLElement = HTMLElement> = T & {
+  ref?: U | null;
+};

+ 27 - 0
packages/shadcn-svelte/src/ui/accordion/accordion-content.svelte

@@ -0,0 +1,27 @@
+<script lang="ts">
+	import { Accordion as AccordionPrimitive } from "bits-ui";
+	import { cn, type WithoutChild } from "../../lib/utils.js";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		children,
+		...restProps
+	}: WithoutChild<AccordionPrimitive.ContentProps> = $props();
+</script>
+
+<AccordionPrimitive.Content
+	bind:ref
+	data-slot="accordion-content"
+	class="data-open:animate-accordion-down data-closed:animate-accordion-up text-sm overflow-hidden"
+	{...restProps}
+>
+	<div
+		class={cn(
+			"pt-0 pb-4 [&_a]:hover:text-foreground [&_a]:underline [&_a]:underline-offset-3 [&_p:not(:last-child)]:mb-4",
+			className
+		)}
+	>
+		{@render children?.()}
+	</div>
+</AccordionPrimitive.Content>

+ 17 - 0
packages/shadcn-svelte/src/ui/accordion/accordion-item.svelte

@@ -0,0 +1,17 @@
+<script lang="ts">
+	import { Accordion as AccordionPrimitive } from "bits-ui";
+	import { cn } from "../../lib/utils.js";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		...restProps
+	}: AccordionPrimitive.ItemProps = $props();
+</script>
+
+<AccordionPrimitive.Item
+	bind:ref
+	data-slot="accordion-item"
+	class={cn("not-last:border-b", className)}
+	{...restProps}
+/>

+ 32 - 0
packages/shadcn-svelte/src/ui/accordion/accordion-trigger.svelte

@@ -0,0 +1,32 @@
+<script lang="ts">
+	import { Accordion as AccordionPrimitive } from "bits-ui";
+	import { cn, type WithoutChild } from "../../lib/utils.js";
+	import ChevronDownIcon from '@lucide/svelte/icons/chevron-down';
+	import ChevronUpIcon from '@lucide/svelte/icons/chevron-up';
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		level = 3,
+		children,
+		...restProps
+	}: WithoutChild<AccordionPrimitive.TriggerProps> & {
+		level?: AccordionPrimitive.HeaderProps["level"];
+	} = $props();
+</script>
+
+<AccordionPrimitive.Header {level} class="flex">
+	<AccordionPrimitive.Trigger
+		data-slot="accordion-trigger"
+		bind:ref
+		class={cn(
+			"focus-visible:ring-ring/50 focus-visible:border-ring focus-visible:after:border-ring **:data-[slot=accordion-trigger-icon]:text-muted-foreground rounded-md py-4 text-left text-sm font-medium hover:underline focus-visible:ring-3 **:data-[slot=accordion-trigger-icon]:ml-auto **:data-[slot=accordion-trigger-icon]:size-4 group/accordion-trigger relative flex flex-1 items-start justify-between border border-transparent transition-all outline-none disabled:pointer-events-none disabled:opacity-50",
+			className
+		)}
+		{...restProps}
+	>
+		{@render children?.()}
+		<ChevronDownIcon data-slot="accordion-trigger-icon" class="cn-accordion-trigger-icon pointer-events-none shrink-0 group-aria-expanded/accordion-trigger:hidden" />
+		<ChevronUpIcon data-slot="accordion-trigger-icon" class="cn-accordion-trigger-icon pointer-events-none hidden shrink-0 group-aria-expanded/accordion-trigger:inline" />
+	</AccordionPrimitive.Trigger>
+</AccordionPrimitive.Header>

+ 19 - 0
packages/shadcn-svelte/src/ui/accordion/accordion.svelte

@@ -0,0 +1,19 @@
+<script lang="ts">
+	import { Accordion as AccordionPrimitive } from "bits-ui";
+	import { cn } from "../../lib/utils.js";
+
+	let {
+		ref = $bindable(null),
+		value = $bindable(),
+		class: className,
+		...restProps
+	}: AccordionPrimitive.RootProps = $props();
+</script>
+
+<AccordionPrimitive.Root
+	bind:ref
+	bind:value={value as never}
+	data-slot="accordion"
+	class={cn("cn-accordion flex w-full flex-col", className)}
+	{...restProps}
+/>

+ 16 - 0
packages/shadcn-svelte/src/ui/accordion/index.ts

@@ -0,0 +1,16 @@
+import Root from "./accordion.svelte";
+import Content from "./accordion-content.svelte";
+import Item from "./accordion-item.svelte";
+import Trigger from "./accordion-trigger.svelte";
+
+export {
+  Root,
+  Content,
+  Item,
+  Trigger,
+  //
+  Root as Accordion,
+  Content as AccordionContent,
+  Item as AccordionItem,
+  Trigger as AccordionTrigger,
+};

+ 20 - 0
packages/shadcn-svelte/src/ui/alert/alert-action.svelte

@@ -0,0 +1,20 @@
+<script lang="ts">
+	import type { HTMLAttributes } from "svelte/elements";
+	import { cn, type WithElementRef } from "../../lib/utils.js";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		children,
+		...restProps
+	}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
+</script>
+
+<div
+	bind:this={ref}
+	data-slot="alert-action"
+	class={cn("absolute top-2.5 right-3", className)}
+	{...restProps}
+>
+	{@render children?.()}
+</div>

+ 23 - 0
packages/shadcn-svelte/src/ui/alert/alert-description.svelte

@@ -0,0 +1,23 @@
+<script lang="ts">
+	import type { HTMLAttributes } from "svelte/elements";
+	import { cn, type WithElementRef } from "../../lib/utils.js";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		children,
+		...restProps
+	}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
+</script>
+
+<div
+	bind:this={ref}
+	data-slot="alert-description"
+	class={cn(
+		"text-muted-foreground text-sm text-balance md:text-pretty [&_p:not(:last-child)]:mb-4 [&_a]:hover:text-foreground [&_a]:underline [&_a]:underline-offset-3",
+		className
+	)}
+	{...restProps}
+>
+	{@render children?.()}
+</div>

+ 23 - 0
packages/shadcn-svelte/src/ui/alert/alert-title.svelte

@@ -0,0 +1,23 @@
+<script lang="ts">
+	import type { HTMLAttributes } from "svelte/elements";
+	import { cn, type WithElementRef } from "../../lib/utils.js";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		children,
+		...restProps
+	}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
+</script>
+
+<div
+	bind:this={ref}
+	data-slot="alert-title"
+	class={cn(
+		"font-medium group-has-[>svg]/alert:col-start-2 [&_a]:hover:text-foreground [&_a]:underline [&_a]:underline-offset-3",
+		className
+	)}
+	{...restProps}
+>
+	{@render children?.()}
+</div>

+ 43 - 0
packages/shadcn-svelte/src/ui/alert/alert.svelte

@@ -0,0 +1,43 @@
+<script lang="ts" module>
+	import { type VariantProps, tv } from "tailwind-variants";
+
+	export const alertVariants = tv({
+		base: "grid gap-0.5 rounded-lg border px-4 py-3 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2.5 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4 group/alert relative w-full",
+		variants: {
+			variant: {
+				default: "bg-card text-card-foreground",
+				destructive: "text-destructive bg-card *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current",
+			},
+		},
+		defaultVariants: {
+			variant: "default",
+		},
+	});
+
+	export type AlertVariant = VariantProps<typeof alertVariants>["variant"];
+</script>
+
+<script lang="ts">
+	import type { HTMLAttributes } from "svelte/elements";
+	import { cn, type WithElementRef } from "../../lib/utils.js";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		variant = "default",
+		children,
+		...restProps
+	}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
+		variant?: AlertVariant;
+	} = $props();
+</script>
+
+<div
+	bind:this={ref}
+	data-slot="alert"
+	role="alert"
+	class={cn(alertVariants({ variant }), className)}
+	{...restProps}
+>
+	{@render children?.()}
+</div>

+ 17 - 0
packages/shadcn-svelte/src/ui/alert/index.ts

@@ -0,0 +1,17 @@
+import Root from "./alert.svelte";
+import Description from "./alert-description.svelte";
+import Title from "./alert-title.svelte";
+import Action from "./alert-action.svelte";
+export { alertVariants, type AlertVariant } from "./alert.svelte";
+
+export {
+  Root,
+  Description,
+  Title,
+  Action,
+  //
+  Root as Alert,
+  Description as AlertDescription,
+  Title as AlertTitle,
+  Action as AlertAction,
+};

+ 49 - 0
packages/shadcn-svelte/src/ui/badge/badge.svelte

@@ -0,0 +1,49 @@
+<script lang="ts" module>
+	import { type VariantProps, tv } from "tailwind-variants";
+
+	export const badgeVariants = tv({
+		base: "h-5 gap-1 rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium transition-all has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:size-3! focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive group/badge inline-flex w-fit shrink-0 items-center justify-center overflow-hidden whitespace-nowrap transition-colors focus-visible:ring-[3px] [&>svg]:pointer-events-none",
+		variants: {
+			variant: {
+				default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
+				secondary: "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
+				destructive: "bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20",
+				outline: "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
+				ghost: "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
+				link: "text-primary underline-offset-4 hover:underline",
+			},
+		},
+		defaultVariants: {
+			variant: "default",
+		},
+	});
+
+	export type BadgeVariant = VariantProps<typeof badgeVariants>["variant"];
+</script>
+
+<script lang="ts">
+	import type { HTMLAnchorAttributes } from "svelte/elements";
+	import { cn, type WithElementRef } from "../../lib/utils.js";
+
+	let {
+		ref = $bindable(null),
+		href,
+		class: className,
+		variant = "default",
+		children,
+		...restProps
+	}: WithElementRef<HTMLAnchorAttributes> & {
+		variant?: BadgeVariant;
+	} = $props();
+</script>
+
+<svelte:element
+	this={href ? "a" : "span"}
+	bind:this={ref}
+	data-slot="badge"
+	{href}
+	class={cn(badgeVariants({ variant }), className)}
+	{...restProps}
+>
+	{@render children?.()}
+</svelte:element>

+ 2 - 0
packages/shadcn-svelte/src/ui/badge/index.ts

@@ -0,0 +1,2 @@
+export { default as Badge } from "./badge.svelte";
+export { badgeVariants, type BadgeVariant } from "./badge.svelte";

+ 82 - 0
packages/shadcn-svelte/src/ui/button/button.svelte

@@ -0,0 +1,82 @@
+<script lang="ts" module>
+	import { cn, type WithElementRef } from "../../lib/utils.js";
+	import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
+	import { type VariantProps, tv } from "tailwind-variants";
+
+	export const buttonVariants = tv({
+		base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-md border border-transparent bg-clip-padding text-sm font-medium focus-visible:ring-3 active:translate-y-px aria-invalid:ring-3 [&_svg:not([class*='size-'])]:size-4 group/button inline-flex shrink-0 items-center justify-center whitespace-nowrap transition-all outline-none select-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
+		variants: {
+			variant: {
+				default: "bg-primary text-primary-foreground hover:bg-primary/80",
+				outline: "border-border bg-background hover:bg-muted hover:text-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground shadow-xs",
+				secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
+				ghost: "hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground",
+				destructive: "bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30",
+				link: "text-primary underline-offset-4 hover:underline",
+			},
+			size: {
+				default: "h-9 gap-1.5 px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
+				xs: "h-6 gap-1 rounded-[min(var(--radius-md),8px)] px-2 text-xs in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
+				sm: "h-8 gap-1 rounded-[min(var(--radius-md),10px)] px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5",
+				lg: "h-10 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
+				icon: "size-9",
+				"icon-xs": "size-6 rounded-[min(var(--radius-md),8px)] in-data-[slot=button-group]:rounded-md [&_svg:not([class*='size-'])]:size-3",
+				"icon-sm": "size-8 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-md",
+				"icon-lg": "size-10",
+			},
+		},
+		defaultVariants: {
+			variant: "default",
+			size: "default",
+		},
+	});
+
+	export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
+	export type ButtonSize = VariantProps<typeof buttonVariants>["size"];
+
+	export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
+		WithElementRef<HTMLAnchorAttributes> & {
+			variant?: ButtonVariant;
+			size?: ButtonSize;
+		};
+</script>
+
+<script lang="ts">
+	let {
+		class: className,
+		variant = "default",
+		size = "default",
+		ref = $bindable(null),
+		href = undefined,
+		type = "button",
+		disabled,
+		children,
+		...restProps
+	}: ButtonProps = $props();
+</script>
+
+{#if href}
+	<a
+		bind:this={ref}
+		data-slot="button"
+		class={cn(buttonVariants({ variant, size }), className)}
+		href={disabled ? undefined : href}
+		aria-disabled={disabled}
+		role={disabled ? "link" : undefined}
+		tabindex={disabled ? -1 : undefined}
+		{...restProps}
+	>
+		{@render children?.()}
+	</a>
+{:else}
+	<button
+		bind:this={ref}
+		data-slot="button"
+		class={cn(buttonVariants({ variant, size }), className)}
+		{type}
+		{disabled}
+		{...restProps}
+	>
+		{@render children?.()}
+	</button>
+{/if}

+ 17 - 0
packages/shadcn-svelte/src/ui/button/index.ts

@@ -0,0 +1,17 @@
+import Root, {
+  type ButtonProps,
+  type ButtonSize,
+  type ButtonVariant,
+  buttonVariants,
+} from "./button.svelte";
+
+export {
+  Root,
+  type ButtonProps as Props,
+  //
+  Root as Button,
+  buttonVariants,
+  type ButtonProps,
+  type ButtonSize,
+  type ButtonVariant,
+};

+ 23 - 0
packages/shadcn-svelte/src/ui/card/card-action.svelte

@@ -0,0 +1,23 @@
+<script lang="ts">
+	import { cn, type WithElementRef } from "../../lib/utils.js";
+	import type { HTMLAttributes } from "svelte/elements";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		children,
+		...restProps
+	}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
+</script>
+
+<div
+	bind:this={ref}
+	data-slot="card-action"
+	class={cn(
+		"cn-card-action col-start-2 row-span-2 row-start-1 self-start justify-self-end",
+		className
+	)}
+	{...restProps}
+>
+	{@render children?.()}
+</div>

+ 20 - 0
packages/shadcn-svelte/src/ui/card/card-content.svelte

@@ -0,0 +1,20 @@
+<script lang="ts">
+	import type { HTMLAttributes } from "svelte/elements";
+	import { cn, type WithElementRef } from "../../lib/utils.js";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		children,
+		...restProps
+	}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
+</script>
+
+<div
+	bind:this={ref}
+	data-slot="card-content"
+	class={cn("px-6 group-data-[size=sm]/card:px-4", className)}
+	{...restProps}
+>
+	{@render children?.()}
+</div>

+ 20 - 0
packages/shadcn-svelte/src/ui/card/card-description.svelte

@@ -0,0 +1,20 @@
+<script lang="ts">
+	import type { HTMLAttributes } from "svelte/elements";
+	import { cn, type WithElementRef } from "../../lib/utils.js";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		children,
+		...restProps
+	}: WithElementRef<HTMLAttributes<HTMLParagraphElement>> = $props();
+</script>
+
+<p
+	bind:this={ref}
+	data-slot="card-description"
+	class={cn("text-muted-foreground text-sm", className)}
+	{...restProps}
+>
+	{@render children?.()}
+</p>

+ 20 - 0
packages/shadcn-svelte/src/ui/card/card-footer.svelte

@@ -0,0 +1,20 @@
+<script lang="ts">
+	import { cn, type WithElementRef } from "../../lib/utils.js";
+	import type { HTMLAttributes } from "svelte/elements";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		children,
+		...restProps
+	}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
+</script>
+
+<div
+	bind:this={ref}
+	data-slot="card-footer"
+	class={cn("rounded-b-xl px-6 group-data-[size=sm]/card:px-4 [.border-t]:pt-6 group-data-[size=sm]/card:[.border-t]:pt-4 flex items-center", className)}
+	{...restProps}
+>
+	{@render children?.()}
+</div>

+ 23 - 0
packages/shadcn-svelte/src/ui/card/card-header.svelte

@@ -0,0 +1,23 @@
+<script lang="ts">
+	import { cn, type WithElementRef } from "../../lib/utils.js";
+	import type { HTMLAttributes } from "svelte/elements";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		children,
+		...restProps
+	}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
+</script>
+
+<div
+	bind:this={ref}
+	data-slot="card-header"
+	class={cn(
+		"gap-1 rounded-t-xl px-6 group-data-[size=sm]/card:px-4 [.border-b]:pb-6 group-data-[size=sm]/card:[.border-b]:pb-4 group/card-header @container/card-header grid auto-rows-min items-start has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto]",
+		className
+	)}
+	{...restProps}
+>
+	{@render children?.()}
+</div>

+ 15 - 0
packages/shadcn-svelte/src/ui/card/card-title.svelte

@@ -0,0 +1,15 @@
+<script lang="ts">
+	import type { HTMLAttributes } from "svelte/elements";
+	import { cn, type WithElementRef } from "../../lib/utils.js";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		children,
+		...restProps
+	}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
+</script>
+
+<div bind:this={ref} data-slot="card-title" class={cn("text-base leading-normal font-medium group-data-[size=sm]/card:text-sm", className)} {...restProps}>
+	{@render children?.()}
+</div>

+ 22 - 0
packages/shadcn-svelte/src/ui/card/card.svelte

@@ -0,0 +1,22 @@
+<script lang="ts">
+	import type { HTMLAttributes } from "svelte/elements";
+	import { cn, type WithElementRef } from "../../lib/utils.js";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		children,
+		size = "default",
+		...restProps
+	}: WithElementRef<HTMLAttributes<HTMLDivElement>> & { size?: "default" | "sm" } = $props();
+</script>
+
+<div
+	bind:this={ref}
+	data-slot="card"
+	data-size={size}
+	class={cn("ring-foreground/10 bg-card text-card-foreground gap-6 overflow-hidden rounded-xl py-6 text-sm shadow-xs ring-1 has-[>img:first-child]:pt-0 data-[size=sm]:gap-4 data-[size=sm]:py-4 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl group/card flex flex-col", className)}
+	{...restProps}
+>
+	{@render children?.()}
+</div>

+ 25 - 0
packages/shadcn-svelte/src/ui/card/index.ts

@@ -0,0 +1,25 @@
+import Root from "./card.svelte";
+import Content from "./card-content.svelte";
+import Description from "./card-description.svelte";
+import Footer from "./card-footer.svelte";
+import Header from "./card-header.svelte";
+import Title from "./card-title.svelte";
+import Action from "./card-action.svelte";
+
+export {
+  Root,
+  Content,
+  Description,
+  Footer,
+  Header,
+  Title,
+  Action,
+  //
+  Root as Card,
+  Content as CardContent,
+  Description as CardDescription,
+  Footer as CardFooter,
+  Header as CardHeader,
+  Title as CardTitle,
+  Action as CardAction,
+};

+ 43 - 0
packages/shadcn-svelte/src/ui/carousel/carousel-content.svelte

@@ -0,0 +1,43 @@
+<script lang="ts">
+	import emblaCarouselSvelte from "embla-carousel-svelte";
+	import type { HTMLAttributes } from "svelte/elements";
+	import { getEmblaContext } from "./context.js";
+	import { cn, type WithElementRef } from "../../lib/utils.js";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		children,
+		...restProps
+	}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
+
+	const emblaCtx = getEmblaContext("<Carousel.Content/>");
+</script>
+
+<div
+	data-slot="carousel-content"
+	class="overflow-hidden"
+	use:emblaCarouselSvelte={{
+		options: {
+			container: "[data-embla-container]",
+			slides: "[data-embla-slide]",
+			...emblaCtx.options,
+			axis: emblaCtx.orientation === "horizontal" ? "x" : "y",
+		},
+		plugins: emblaCtx.plugins,
+	}}
+	onemblaInit={emblaCtx.onInit}
+>
+	<div
+		bind:this={ref}
+		class={cn(
+			"flex",
+			emblaCtx.orientation === "horizontal" ? "-ms-4" : "-mt-4 flex-col",
+			className
+		)}
+		data-embla-container=""
+		{...restProps}
+	>
+		{@render children?.()}
+	</div>
+</div>

+ 30 - 0
packages/shadcn-svelte/src/ui/carousel/carousel-item.svelte

@@ -0,0 +1,30 @@
+<script lang="ts">
+	import type { HTMLAttributes } from "svelte/elements";
+	import { getEmblaContext } from "./context.js";
+	import { cn, type WithElementRef } from "../../lib/utils.js";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		children,
+		...restProps
+	}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
+
+	const emblaCtx = getEmblaContext("<Carousel.Item/>");
+</script>
+
+<div
+	bind:this={ref}
+	data-slot="carousel-item"
+	role="group"
+	aria-roledescription="slide"
+	class={cn(
+		"min-w-0 shrink-0 grow-0 basis-full",
+		emblaCtx.orientation === "horizontal" ? "ps-4" : "pt-4",
+		className
+	)}
+	data-embla-slide=""
+	{...restProps}
+>
+	{@render children?.()}
+</div>

+ 39 - 0
packages/shadcn-svelte/src/ui/carousel/carousel-next.svelte

@@ -0,0 +1,39 @@
+<script lang="ts">
+	import type { WithoutChildren } from "bits-ui";
+	import { getEmblaContext } from "./context.js";
+	import { cn } from "../../lib/utils.js";
+	import { Button, type Props } from "../button/index.js";
+	import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		variant = "outline",
+		size = "icon-sm",
+		...restProps
+	}: WithoutChildren<Props> = $props();
+
+	const emblaCtx = getEmblaContext("<Carousel.Next/>");
+</script>
+
+<Button
+	data-slot="carousel-next"
+	{variant}
+	{size}
+	aria-disabled={!emblaCtx.canScrollNext}
+	disabled={!emblaCtx.canScrollNext}
+	class={cn(
+		"rounded-full absolute touch-manipulation",
+		emblaCtx.orientation === "horizontal"
+			? "-end-12 top-1/2 -translate-y-1/2"
+			: "start-1/2 -bottom-12 -translate-x-1/2 rotate-90",
+		className
+	)}
+	onclick={emblaCtx.scrollNext}
+	onkeydown={emblaCtx.handleKeyDown}
+	bind:ref
+	{...restProps}
+>
+	<ChevronRightIcon  />
+	<span class="sr-only">Next slide</span>
+</Button>

+ 39 - 0
packages/shadcn-svelte/src/ui/carousel/carousel-previous.svelte

@@ -0,0 +1,39 @@
+<script lang="ts">
+	import type { WithoutChildren } from "bits-ui";
+	import { getEmblaContext } from "./context.js";
+	import { cn } from "../../lib/utils.js";
+	import { Button, type Props } from "../button/index.js";
+	import ChevronLeftIcon from '@lucide/svelte/icons/chevron-left';
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		variant = "outline",
+		size = "icon-sm",
+		...restProps
+	}: WithoutChildren<Props> = $props();
+
+	const emblaCtx = getEmblaContext("<Carousel.Previous/>");
+</script>
+
+<Button
+	data-slot="carousel-previous"
+	{variant}
+	{size}
+	aria-disabled={!emblaCtx.canScrollPrev}
+	disabled={!emblaCtx.canScrollPrev}
+	class={cn(
+		"rounded-full absolute touch-manipulation",
+		emblaCtx.orientation === "horizontal"
+			? "-start-12 top-1/2 -translate-y-1/2"
+			: "start-1/2 -top-12 -translate-x-1/2 rotate-90",
+		className
+	)}
+	onclick={emblaCtx.scrollPrev}
+	onkeydown={emblaCtx.handleKeyDown}
+	{...restProps}
+	bind:ref
+>
+	<ChevronLeftIcon  />
+	<span class="sr-only">Previous slide</span>
+</Button>

+ 94 - 0
packages/shadcn-svelte/src/ui/carousel/carousel.svelte

@@ -0,0 +1,94 @@
+<script lang="ts">
+	import {
+		type CarouselAPI,
+		type CarouselProps,
+		type EmblaContext,
+		setEmblaContext,
+	} from "./context.js";
+	import { cn, type WithElementRef } from "../../lib/utils.js";
+
+	let {
+		ref = $bindable(null),
+		opts = {},
+		plugins = [],
+		setApi = () => {},
+		orientation = "horizontal",
+		class: className,
+		children,
+		...restProps
+	}: WithElementRef<CarouselProps> = $props();
+
+	// svelte-ignore state_referenced_locally
+	let carouselState = $state<EmblaContext>({
+		api: undefined,
+		scrollPrev,
+		scrollNext,
+		orientation,
+		canScrollNext: false,
+		canScrollPrev: false,
+		handleKeyDown,
+		options: opts,
+		plugins,
+		onInit,
+		scrollSnaps: [],
+		selectedIndex: 0,
+		scrollTo,
+	});
+
+	setEmblaContext(carouselState);
+
+	function scrollPrev() {
+		carouselState.api?.scrollPrev();
+	}
+
+	function scrollNext() {
+		carouselState.api?.scrollNext();
+	}
+
+	function scrollTo(index: number, jump?: boolean) {
+		carouselState.api?.scrollTo(index, jump);
+	}
+
+	function onSelect() {
+		if (!carouselState.api) return;
+		carouselState.selectedIndex = carouselState.api.selectedScrollSnap();
+		carouselState.canScrollNext = carouselState.api.canScrollNext();
+		carouselState.canScrollPrev = carouselState.api.canScrollPrev();
+	}
+
+	function handleKeyDown(e: KeyboardEvent) {
+		if (e.key === "ArrowLeft") {
+			e.preventDefault();
+			scrollPrev();
+		} else if (e.key === "ArrowRight") {
+			e.preventDefault();
+			scrollNext();
+		}
+	}
+
+	function onInit(event: CustomEvent<CarouselAPI>) {
+		carouselState.api = event.detail;
+		setApi(carouselState.api);
+
+		carouselState.scrollSnaps = carouselState.api.scrollSnapList();
+		carouselState.api.on("select", onSelect);
+		onSelect();
+	}
+
+	$effect(() => {
+		return () => {
+			carouselState.api?.off("select", onSelect);
+		};
+	});
+</script>
+
+<div
+	bind:this={ref}
+	data-slot="carousel"
+	class={cn("relative", className)}
+	role="region"
+	aria-roledescription="carousel"
+	{...restProps}
+>
+	{@render children?.()}
+</div>

+ 60 - 0
packages/shadcn-svelte/src/ui/carousel/context.ts

@@ -0,0 +1,60 @@
+import type { WithElementRef } from "../../lib/utils.js";
+import type {
+  EmblaCarouselSvelteType,
+  default as emblaCarouselSvelte,
+} from "embla-carousel-svelte";
+import { getContext, hasContext, setContext } from "svelte";
+import type { HTMLAttributes } from "svelte/elements";
+
+export type CarouselAPI =
+  NonNullable<
+    NonNullable<EmblaCarouselSvelteType["$$_attributes"]>["on:emblaInit"]
+  > extends (evt: CustomEvent<infer CarouselAPI>) => void
+    ? CarouselAPI
+    : never;
+
+type EmblaCarouselConfig = NonNullable<
+  Parameters<typeof emblaCarouselSvelte>[1]
+>;
+
+export type CarouselOptions = EmblaCarouselConfig["options"];
+export type CarouselPlugins = EmblaCarouselConfig["plugins"];
+
+////
+
+export type CarouselProps = {
+  opts?: CarouselOptions;
+  plugins?: CarouselPlugins;
+  setApi?: (api: CarouselAPI | undefined) => void;
+  orientation?: "horizontal" | "vertical";
+} & WithElementRef<HTMLAttributes<HTMLDivElement>>;
+
+const EMBLA_CAROUSEL_CONTEXT = Symbol("EMBLA_CAROUSEL_CONTEXT");
+
+export type EmblaContext = {
+  api: CarouselAPI | undefined;
+  orientation: "horizontal" | "vertical";
+  scrollNext: () => void;
+  scrollPrev: () => void;
+  canScrollNext: boolean;
+  canScrollPrev: boolean;
+  handleKeyDown: (e: KeyboardEvent) => void;
+  options: CarouselOptions;
+  plugins: CarouselPlugins;
+  onInit: (e: CustomEvent<CarouselAPI>) => void;
+  scrollTo: (index: number, jump?: boolean) => void;
+  scrollSnaps: number[];
+  selectedIndex: number;
+};
+
+export function setEmblaContext(config: EmblaContext): EmblaContext {
+  setContext(EMBLA_CAROUSEL_CONTEXT, config);
+  return config;
+}
+
+export function getEmblaContext(name = "This component") {
+  if (!hasContext(EMBLA_CAROUSEL_CONTEXT)) {
+    throw new Error(`${name} must be used within a <Carousel.Root> component`);
+  }
+  return getContext<ReturnType<typeof setEmblaContext>>(EMBLA_CAROUSEL_CONTEXT);
+}

+ 19 - 0
packages/shadcn-svelte/src/ui/carousel/index.ts

@@ -0,0 +1,19 @@
+import Root from "./carousel.svelte";
+import Content from "./carousel-content.svelte";
+import Item from "./carousel-item.svelte";
+import Previous from "./carousel-previous.svelte";
+import Next from "./carousel-next.svelte";
+
+export {
+  Root,
+  Content,
+  Item,
+  Previous,
+  Next,
+  //
+  Root as Carousel,
+  Content as CarouselContent,
+  Item as CarouselItem,
+  Previous as CarouselPrevious,
+  Next as CarouselNext,
+};

+ 39 - 0
packages/shadcn-svelte/src/ui/checkbox/checkbox.svelte

@@ -0,0 +1,39 @@
+<script lang="ts">
+	import { Checkbox as CheckboxPrimitive } from "bits-ui";
+	import { cn, type WithoutChildrenOrChild } from "../../lib/utils.js";
+	import CheckIcon from '@lucide/svelte/icons/check';
+	import MinusIcon from '@lucide/svelte/icons/minus';
+
+	let {
+		ref = $bindable(null),
+		checked = $bindable(false),
+		indeterminate = $bindable(false),
+		class: className,
+		...restProps
+	}: WithoutChildrenOrChild<CheckboxPrimitive.RootProps> = $props();
+</script>
+
+<CheckboxPrimitive.Root
+	bind:ref
+	data-slot="checkbox"
+	class={cn(
+		"border-input dark:bg-input/30 data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary data-checked:border-primary aria-invalid:aria-checked:border-primary aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 flex size-4 items-center justify-center rounded-[4px] border shadow-xs transition-shadow group-has-disabled/field:opacity-50 focus-visible:ring-3 aria-invalid:ring-3 peer relative shrink-0 outline-none after:absolute after:-inset-x-3 after:-inset-y-2 disabled:cursor-not-allowed disabled:opacity-50",
+		className
+	)}
+	bind:checked
+	bind:indeterminate
+	{...restProps}
+>
+	{#snippet children({ checked, indeterminate })}
+		<div
+			data-slot="checkbox-indicator"
+			class="[&>svg]:size-3.5 grid place-content-center text-current transition-none"
+		>
+			{#if checked}
+				<CheckIcon  />
+			{:else if indeterminate}
+				<MinusIcon  />
+			{/if}
+		</div>
+	{/snippet}
+</CheckboxPrimitive.Root>

+ 6 - 0
packages/shadcn-svelte/src/ui/checkbox/index.ts

@@ -0,0 +1,6 @@
+import Root from "./checkbox.svelte";
+export {
+  Root,
+  //
+  Root as Checkbox,
+};

+ 7 - 0
packages/shadcn-svelte/src/ui/collapsible/collapsible-content.svelte

@@ -0,0 +1,7 @@
+<script lang="ts">
+	import { Collapsible as CollapsiblePrimitive } from "bits-ui";
+
+	let { ref = $bindable(null), ...restProps }: CollapsiblePrimitive.ContentProps = $props();
+</script>
+
+<CollapsiblePrimitive.Content bind:ref data-slot="collapsible-content" {...restProps} />

+ 7 - 0
packages/shadcn-svelte/src/ui/collapsible/collapsible-trigger.svelte

@@ -0,0 +1,7 @@
+<script lang="ts">
+	import { Collapsible as CollapsiblePrimitive } from "bits-ui";
+
+	let { ref = $bindable(null), ...restProps }: CollapsiblePrimitive.TriggerProps = $props();
+</script>
+
+<CollapsiblePrimitive.Trigger bind:ref data-slot="collapsible-trigger" {...restProps} />

+ 11 - 0
packages/shadcn-svelte/src/ui/collapsible/collapsible.svelte

@@ -0,0 +1,11 @@
+<script lang="ts">
+	import { Collapsible as CollapsiblePrimitive } from "bits-ui";
+
+	let {
+		ref = $bindable(null),
+		open = $bindable(false),
+		...restProps
+	}: CollapsiblePrimitive.RootProps = $props();
+</script>
+
+<CollapsiblePrimitive.Root bind:ref bind:open data-slot="collapsible" {...restProps} />

+ 13 - 0
packages/shadcn-svelte/src/ui/collapsible/index.ts

@@ -0,0 +1,13 @@
+import Root from "./collapsible.svelte";
+import Trigger from "./collapsible-trigger.svelte";
+import Content from "./collapsible-content.svelte";
+
+export {
+  Root,
+  Content,
+  Trigger,
+  //
+  Root as Collapsible,
+  Content as CollapsibleContent,
+  Trigger as CollapsibleTrigger,
+};

+ 11 - 0
packages/shadcn-svelte/src/ui/dialog/dialog-close.svelte

@@ -0,0 +1,11 @@
+<script lang="ts">
+	import { Dialog as DialogPrimitive } from "bits-ui";
+
+	let {
+		ref = $bindable(null),
+		type = "button",
+		...restProps
+	}: DialogPrimitive.CloseProps = $props();
+</script>
+
+<DialogPrimitive.Close bind:ref data-slot="dialog-close" {type} {...restProps} />

+ 48 - 0
packages/shadcn-svelte/src/ui/dialog/dialog-content.svelte

@@ -0,0 +1,48 @@
+<script lang="ts">
+	import { Dialog as DialogPrimitive } from "bits-ui";
+	import DialogPortal from "./dialog-portal.svelte";
+	import type { Snippet } from "svelte";
+	import * as Dialog from "./index.js";
+	import { cn, type WithoutChildrenOrChild } from "../../lib/utils.js";
+	import type { ComponentProps } from "svelte";
+	import { Button } from "../button/index.js";
+	import XIcon from '@lucide/svelte/icons/x';
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		portalProps,
+		children,
+		showCloseButton = true,
+		...restProps
+	}: WithoutChildrenOrChild<DialogPrimitive.ContentProps> & {
+		portalProps?: WithoutChildrenOrChild<ComponentProps<typeof DialogPortal>>;
+		children: Snippet;
+		showCloseButton?: boolean;
+	} = $props();
+</script>
+
+<DialogPortal {...portalProps}>
+	<Dialog.Overlay />
+	<DialogPrimitive.Content
+		bind:ref
+		data-slot="dialog-content"
+		class={cn(
+			"bg-background data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 ring-foreground/10 grid max-w-[calc(100%-2rem)] gap-6 rounded-xl p-6 text-sm ring-1 duration-100 sm:max-w-md fixed top-1/2 left-1/2 z-50 w-full -translate-x-1/2 -translate-y-1/2 outline-none",
+			className
+		)}
+		{...restProps}
+	>
+		{@render children?.()}
+		{#if showCloseButton}
+			<DialogPrimitive.Close data-slot="dialog-close">
+				{#snippet child({ props })}
+					<Button variant="ghost" class="absolute top-4 right-4" size="icon-sm" {...props}>
+						<XIcon  />
+						<span class="sr-only">Close</span>
+					</Button>
+				{/snippet}
+			</DialogPrimitive.Close>
+		{/if}
+	</DialogPrimitive.Content>
+</DialogPortal>

+ 17 - 0
packages/shadcn-svelte/src/ui/dialog/dialog-description.svelte

@@ -0,0 +1,17 @@
+<script lang="ts">
+	import { Dialog as DialogPrimitive } from "bits-ui";
+	import { cn } from "../../lib/utils.js";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		...restProps
+	}: DialogPrimitive.DescriptionProps = $props();
+</script>
+
+<DialogPrimitive.Description
+	bind:ref
+	data-slot="dialog-description"
+	class={cn("text-muted-foreground *:[a]:hover:text-foreground text-sm *:[a]:underline *:[a]:underline-offset-3", className)}
+	{...restProps}
+/>

+ 32 - 0
packages/shadcn-svelte/src/ui/dialog/dialog-footer.svelte

@@ -0,0 +1,32 @@
+<script lang="ts">
+	import { cn, type WithElementRef } from "../../lib/utils.js";
+	import type { HTMLAttributes } from "svelte/elements";
+	import { Dialog as DialogPrimitive } from "bits-ui";
+	import { Button } from "../button/index.js";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		children,
+		showCloseButton = false,
+		...restProps
+	}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
+		showCloseButton?: boolean;
+	} = $props();
+</script>
+
+<div
+	bind:this={ref}
+	data-slot="dialog-footer"
+	class={cn("gap-2 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
+	{...restProps}
+>
+	{@render children?.()}
+	{#if showCloseButton}
+		<DialogPrimitive.Close>
+			{#snippet child({ props })}
+				<Button variant="outline" {...props}>Close</Button>
+			{/snippet}
+		</DialogPrimitive.Close>
+	{/if}
+</div>

+ 20 - 0
packages/shadcn-svelte/src/ui/dialog/dialog-header.svelte

@@ -0,0 +1,20 @@
+<script lang="ts">
+	import type { HTMLAttributes } from "svelte/elements";
+	import { cn, type WithElementRef } from "../../lib/utils.js";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		children,
+		...restProps
+	}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
+</script>
+
+<div
+	bind:this={ref}
+	data-slot="dialog-header"
+	class={cn("gap-2 flex flex-col", className)}
+	{...restProps}
+>
+	{@render children?.()}
+</div>

+ 17 - 0
packages/shadcn-svelte/src/ui/dialog/dialog-overlay.svelte

@@ -0,0 +1,17 @@
+<script lang="ts">
+	import { Dialog as DialogPrimitive } from "bits-ui";
+	import { cn } from "../../lib/utils.js";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		...restProps
+	}: DialogPrimitive.OverlayProps = $props();
+</script>
+
+<DialogPrimitive.Overlay
+	bind:ref
+	data-slot="dialog-overlay"
+	class={cn("data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 isolate z-50", className)}
+	{...restProps}
+/>

+ 7 - 0
packages/shadcn-svelte/src/ui/dialog/dialog-portal.svelte

@@ -0,0 +1,7 @@
+<script lang="ts">
+	import { Dialog as DialogPrimitive } from "bits-ui";
+
+	let { ...restProps }: DialogPrimitive.PortalProps = $props();
+</script>
+
+<DialogPrimitive.Portal {...restProps} />

+ 17 - 0
packages/shadcn-svelte/src/ui/dialog/dialog-title.svelte

@@ -0,0 +1,17 @@
+<script lang="ts">
+	import { Dialog as DialogPrimitive } from "bits-ui";
+	import { cn } from "../../lib/utils.js";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		...restProps
+	}: DialogPrimitive.TitleProps = $props();
+</script>
+
+<DialogPrimitive.Title
+	bind:ref
+	data-slot="dialog-title"
+	class={cn("leading-none font-medium", className)}
+	{...restProps}
+/>

+ 11 - 0
packages/shadcn-svelte/src/ui/dialog/dialog-trigger.svelte

@@ -0,0 +1,11 @@
+<script lang="ts">
+	import { Dialog as DialogPrimitive } from "bits-ui";
+
+	let {
+		ref = $bindable(null),
+		type = "button",
+		...restProps
+	}: DialogPrimitive.TriggerProps = $props();
+</script>
+
+<DialogPrimitive.Trigger bind:ref data-slot="dialog-trigger" {type} {...restProps} />

+ 7 - 0
packages/shadcn-svelte/src/ui/dialog/dialog.svelte

@@ -0,0 +1,7 @@
+<script lang="ts">
+	import { Dialog as DialogPrimitive } from "bits-ui";
+
+	let { open = $bindable(false), ...restProps }: DialogPrimitive.RootProps = $props();
+</script>
+
+<DialogPrimitive.Root bind:open {...restProps} />

+ 34 - 0
packages/shadcn-svelte/src/ui/dialog/index.ts

@@ -0,0 +1,34 @@
+import Root from "./dialog.svelte";
+import Portal from "./dialog-portal.svelte";
+import Title from "./dialog-title.svelte";
+import Footer from "./dialog-footer.svelte";
+import Header from "./dialog-header.svelte";
+import Overlay from "./dialog-overlay.svelte";
+import Content from "./dialog-content.svelte";
+import Description from "./dialog-description.svelte";
+import Trigger from "./dialog-trigger.svelte";
+import Close from "./dialog-close.svelte";
+
+export {
+  Root,
+  Title,
+  Portal,
+  Footer,
+  Header,
+  Trigger,
+  Overlay,
+  Content,
+  Description,
+  Close,
+  //
+  Root as Dialog,
+  Title as DialogTitle,
+  Portal as DialogPortal,
+  Footer as DialogFooter,
+  Header as DialogHeader,
+  Trigger as DialogTrigger,
+  Overlay as DialogOverlay,
+  Content as DialogContent,
+  Description as DialogDescription,
+  Close as DialogClose,
+};

+ 7 - 0
packages/shadcn-svelte/src/ui/drawer/drawer-close.svelte

@@ -0,0 +1,7 @@
+<script lang="ts">
+	import { Drawer as DrawerPrimitive } from "vaul-svelte";
+
+	let { ref = $bindable(null), ...restProps }: DrawerPrimitive.CloseProps = $props();
+</script>
+
+<DrawerPrimitive.Close bind:ref data-slot="drawer-close" {...restProps} />

+ 33 - 0
packages/shadcn-svelte/src/ui/drawer/drawer-content.svelte

@@ -0,0 +1,33 @@
+<script lang="ts">
+	import { Drawer as DrawerPrimitive } from "vaul-svelte";
+	import DrawerPortal from "./drawer-portal.svelte";
+	import DrawerOverlay from "./drawer-overlay.svelte";
+	import { cn } from "../../lib/utils.js";
+	import type { ComponentProps } from "svelte";
+	import type { WithoutChildrenOrChild } from "../../lib/utils.js";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		portalProps,
+		children,
+		...restProps
+	}: DrawerPrimitive.ContentProps & {
+		portalProps?: WithoutChildrenOrChild<ComponentProps<typeof DrawerPortal>>;
+	} = $props();
+</script>
+
+<DrawerPortal {...portalProps}>
+	<DrawerOverlay />
+	<DrawerPrimitive.Content
+		bind:ref
+		data-slot="drawer-content"
+		class={cn("bg-background flex h-auto flex-col text-sm data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-xl data-[vaul-drawer-direction=bottom]:border-t data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:rounded-r-xl data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:rounded-l-xl data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-xl data-[vaul-drawer-direction=top]:border-b data-[vaul-drawer-direction=left]:sm:max-w-sm data-[vaul-drawer-direction=right]:sm:max-w-sm group/drawer-content fixed z-50", className)}
+		{...restProps}
+	>
+		<div
+			class="bg-muted mx-auto mt-4 hidden h-1.5 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block bg-muted mx-auto hidden shrink-0 group-data-[vaul-drawer-direction=bottom]/drawer-content:block"
+		></div>
+		{@render children?.()}
+	</DrawerPrimitive.Content>
+</DrawerPortal>

+ 17 - 0
packages/shadcn-svelte/src/ui/drawer/drawer-description.svelte

@@ -0,0 +1,17 @@
+<script lang="ts">
+	import { Drawer as DrawerPrimitive } from "vaul-svelte";
+	import { cn } from "../../lib/utils.js";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		...restProps
+	}: DrawerPrimitive.DescriptionProps = $props();
+</script>
+
+<DrawerPrimitive.Description
+	bind:ref
+	data-slot="drawer-description"
+	class={cn("text-muted-foreground text-sm", className)}
+	{...restProps}
+/>

+ 20 - 0
packages/shadcn-svelte/src/ui/drawer/drawer-footer.svelte

@@ -0,0 +1,20 @@
+<script lang="ts">
+	import { cn, type WithElementRef } from "../../lib/utils.js";
+	import type { HTMLAttributes } from "svelte/elements";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		children,
+		...restProps
+	}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
+</script>
+
+<div
+	bind:this={ref}
+	data-slot="drawer-footer"
+	class={cn("gap-2 p-4 mt-auto flex flex-col", className)}
+	{...restProps}
+>
+	{@render children?.()}
+</div>

+ 20 - 0
packages/shadcn-svelte/src/ui/drawer/drawer-header.svelte

@@ -0,0 +1,20 @@
+<script lang="ts">
+	import type { HTMLAttributes } from "svelte/elements";
+	import { cn, type WithElementRef } from "../../lib/utils.js";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		children,
+		...restProps
+	}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
+</script>
+
+<div
+	bind:this={ref}
+	data-slot="drawer-header"
+	class={cn("gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.5 md:text-left flex flex-col", className)}
+	{...restProps}
+>
+	{@render children?.()}
+</div>

+ 12 - 0
packages/shadcn-svelte/src/ui/drawer/drawer-nested.svelte

@@ -0,0 +1,12 @@
+<script lang="ts">
+	import { Drawer as DrawerPrimitive } from "vaul-svelte";
+
+	let {
+		shouldScaleBackground = true,
+		open = $bindable(false),
+		activeSnapPoint = $bindable(null),
+		...restProps
+	}: DrawerPrimitive.RootProps = $props();
+</script>
+
+<DrawerPrimitive.NestedRoot {shouldScaleBackground} bind:open bind:activeSnapPoint {...restProps} />

+ 17 - 0
packages/shadcn-svelte/src/ui/drawer/drawer-overlay.svelte

@@ -0,0 +1,17 @@
+<script lang="ts">
+	import { Drawer as DrawerPrimitive } from "vaul-svelte";
+	import { cn } from "../../lib/utils.js";
+
+	let {
+		ref = $bindable(null),
+		class: className,
+		...restProps
+	}: DrawerPrimitive.OverlayProps = $props();
+</script>
+
+<DrawerPrimitive.Overlay
+	bind:ref
+	data-slot="drawer-overlay"
+	class={cn("data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/10 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 z-50", className)}
+	{...restProps}
+/>

+ 7 - 0
packages/shadcn-svelte/src/ui/drawer/drawer-portal.svelte

@@ -0,0 +1,7 @@
+<script lang="ts">
+	import { Drawer as DrawerPrimitive } from "vaul-svelte";
+
+	let { ...restProps }: DrawerPrimitive.PortalProps = $props();
+</script>
+
+<DrawerPrimitive.Portal {...restProps} />

Niektoré súbory nie sú zobrazené, pretože je v týchto rozdielových dátach zmenené mnoho súborov