bakethere 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +74 -0
  2. package/dist/cli.js +5059 -0
  3. package/package.json +33 -0
package/dist/cli.js ADDED
@@ -0,0 +1,5059 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // src/cli.ts
5
+ var import_commander4 = require("commander");
6
+
7
+ // src/commands/list.ts
8
+ var import_commander = require("commander");
9
+ var import_prompts = require("@clack/prompts");
10
+
11
+ // src/registry/components/button.ts
12
+ var button = {
13
+ name: "button",
14
+ category: "primitives",
15
+ description: "Accessible button with variants, sizes, and loading state",
16
+ infraDeps: ["tokens", "globals", "provider", "utils"],
17
+ componentDeps: [],
18
+ npmDeps: [],
19
+ files: [
20
+ {
21
+ name: "Button.tsx",
22
+ content: `"use client";
23
+
24
+ import { forwardRef } from "react";
25
+ import { cn } from "@/lib/utils";
26
+ import { useBakeThereTheme } from "../../provider";
27
+ import type { ButtonProps } from "./button.types";
28
+
29
+ const variantClasses: Record<string, string> = {
30
+ solid:
31
+ "bg-[var(--bt-accent)] text-[var(--bt-text-inverse)] hover:bg-[var(--bt-accent-hover)] border border-transparent",
32
+ outline:
33
+ "bg-transparent text-[var(--bt-accent)] border border-[var(--bt-accent)] hover:bg-[var(--bt-accent-muted)]",
34
+ ghost:
35
+ "bg-transparent text-[var(--bt-text-primary)] border border-transparent hover:bg-[var(--bt-hover-bg)]",
36
+ destructive:
37
+ "bg-[var(--bt-destructive-bg)] text-[var(--bt-destructive)] border border-[var(--bt-destructive)] hover:opacity-80",
38
+ };
39
+
40
+ const sizeClasses: Record<string, string> = {
41
+ sm: "h-8 px-3 text-sm gap-1.5",
42
+ md: "h-10 px-4 text-sm gap-2",
43
+ lg: "h-12 px-6 text-base gap-2",
44
+ icon: "h-10 w-10 p-0",
45
+ };
46
+
47
+ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
48
+ (
49
+ {
50
+ variant = "solid",
51
+ size = "md",
52
+ theme,
53
+ isLoading = false,
54
+ leftIcon,
55
+ rightIcon,
56
+ disabled,
57
+ className,
58
+ children,
59
+ ...props
60
+ },
61
+ ref
62
+ ) => {
63
+ const { theme: contextTheme } = useBakeThereTheme();
64
+ const activeTheme = theme ?? contextTheme;
65
+
66
+ return (
67
+ <button
68
+ ref={ref}
69
+ data-bt-theme={activeTheme}
70
+ disabled={disabled || isLoading}
71
+ className={cn(
72
+ "inline-flex items-center justify-center font-medium rounded-[var(--bt-radius-md)] transition-colors",
73
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--bt-ring)]",
74
+ "disabled:opacity-[var(--bt-disabled-opacity)] disabled:pointer-events-none",
75
+ variantClasses[variant],
76
+ sizeClasses[size],
77
+ className
78
+ )}
79
+ {...props}
80
+ >
81
+ {isLoading ? (
82
+ <svg
83
+ className="animate-spin h-4 w-4"
84
+ xmlns="http://www.w3.org/2000/svg"
85
+ fill="none"
86
+ viewBox="0 0 24 24"
87
+ aria-hidden="true"
88
+ >
89
+ <circle
90
+ className="opacity-25"
91
+ cx="12"
92
+ cy="12"
93
+ r="10"
94
+ stroke="currentColor"
95
+ strokeWidth="4"
96
+ />
97
+ <path
98
+ className="opacity-75"
99
+ fill="currentColor"
100
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
101
+ />
102
+ </svg>
103
+ ) : (
104
+ <>
105
+ {leftIcon && <span aria-hidden="true">{leftIcon}</span>}
106
+ {children}
107
+ {rightIcon && <span aria-hidden="true">{rightIcon}</span>}
108
+ </>
109
+ )}
110
+ </button>
111
+ );
112
+ }
113
+ );
114
+
115
+ Button.displayName = "Button";
116
+ `
117
+ },
118
+ {
119
+ name: "button.types.ts",
120
+ content: `import type { ButtonHTMLAttributes } from "react";
121
+ import type { BtTheme } from "../../types";
122
+
123
+ export type ButtonVariant = "solid" | "outline" | "ghost" | "destructive";
124
+ export type ButtonSize = "sm" | "md" | "lg" | "icon";
125
+
126
+ export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
127
+ variant?: ButtonVariant;
128
+ size?: ButtonSize;
129
+ theme?: BtTheme;
130
+ isLoading?: boolean;
131
+ leftIcon?: React.ReactNode;
132
+ rightIcon?: React.ReactNode;
133
+ }
134
+ `
135
+ }
136
+ ]
137
+ };
138
+
139
+ // src/registry/components/button-group.ts
140
+ var buttonGroup = {
141
+ name: "button-group",
142
+ category: "primitives",
143
+ description: "Group buttons into a visually connected row",
144
+ infraDeps: ["tokens", "globals", "utils"],
145
+ componentDeps: [],
146
+ npmDeps: [],
147
+ files: [
148
+ {
149
+ name: "ButtonGroup.tsx",
150
+ content: `import { cn } from "@/lib/utils";
151
+ import type { ButtonGroupProps } from "./button-group.types";
152
+
153
+ export function ButtonGroup({ className, children, ...props }: ButtonGroupProps) {
154
+ return (
155
+ <div className={cn("bt-button-group", className)} {...props}>
156
+ {children}
157
+ </div>
158
+ );
159
+ }
160
+ `
161
+ },
162
+ {
163
+ name: "button-group.types.ts",
164
+ content: `import type { HTMLAttributes } from "react";
165
+
166
+ export interface ButtonGroupProps extends HTMLAttributes<HTMLDivElement> {}
167
+ `
168
+ }
169
+ ]
170
+ };
171
+
172
+ // src/registry/components/input.ts
173
+ var input = {
174
+ name: "input",
175
+ category: "primitives",
176
+ description: "Text input with label, error state, and prefix/suffix support",
177
+ infraDeps: ["tokens", "globals", "provider", "utils"],
178
+ componentDeps: [],
179
+ npmDeps: [],
180
+ files: [
181
+ {
182
+ name: "Input.tsx",
183
+ content: `"use client";
184
+
185
+ import { forwardRef, useId } from "react";
186
+ import { cn } from "@/lib/utils";
187
+ import { useBakeThereTheme } from "../../provider";
188
+ import type { InputProps } from "./input.types";
189
+
190
+ export const Input = forwardRef<HTMLInputElement, InputProps>(
191
+ (
192
+ {
193
+ theme,
194
+ label,
195
+ helperText,
196
+ errorText,
197
+ leftAddon,
198
+ rightAddon,
199
+ state = "default",
200
+ disabled,
201
+ className,
202
+ id: externalId,
203
+ ...props
204
+ },
205
+ ref
206
+ ) => {
207
+ const { theme: contextTheme } = useBakeThereTheme();
208
+ const activeTheme = theme ?? contextTheme;
209
+ const generatedId = useId();
210
+ const id = externalId ?? generatedId;
211
+
212
+ const isError = state === "error" || !!errorText;
213
+ const isDisabled = state === "disabled" || disabled;
214
+
215
+ return (
216
+ <div data-bt-theme={activeTheme} className="flex flex-col gap-1.5 w-full">
217
+ {label && (
218
+ <label
219
+ htmlFor={id}
220
+ className="text-sm font-medium text-[var(--bt-text-primary)]"
221
+ >
222
+ {label}
223
+ </label>
224
+ )}
225
+ <div className="relative flex items-center">
226
+ {leftAddon && (
227
+ <div className="absolute left-3 text-[var(--bt-text-muted)]">
228
+ {leftAddon}
229
+ </div>
230
+ )}
231
+ <input
232
+ ref={ref}
233
+ id={id}
234
+ disabled={isDisabled}
235
+ aria-invalid={isError || undefined}
236
+ aria-describedby={
237
+ errorText ? \`\${id}-error\` : helperText ? \`\${id}-helper\` : undefined
238
+ }
239
+ className={cn(
240
+ "w-full rounded-[var(--bt-radius-md)] border bg-[var(--bt-bg-surface)]",
241
+ "text-[var(--bt-text-primary)] placeholder:text-[var(--bt-text-muted)]",
242
+ "px-3 py-2 text-sm transition-colors",
243
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--bt-ring)]",
244
+ "disabled:bg-[var(--bt-bg-muted)] disabled:text-[var(--bt-text-muted)] disabled:border-[var(--bt-border-muted)] disabled:pointer-events-none disabled:cursor-not-allowed",
245
+ isError
246
+ ? "border-[var(--bt-destructive)] focus-visible:ring-[var(--bt-destructive)]"
247
+ : "border-[var(--bt-border)] focus-visible:border-[var(--bt-border-focus)]",
248
+ leftAddon ? "pl-9" : "",
249
+ rightAddon ? "pr-9" : "",
250
+ className
251
+ )}
252
+ {...props}
253
+ />
254
+ {rightAddon && (
255
+ <div className="absolute right-3 text-[var(--bt-text-muted)]">
256
+ {rightAddon}
257
+ </div>
258
+ )}
259
+ </div>
260
+ {errorText && (
261
+ <p id={\`\${id}-error\`} className="text-xs text-[var(--bt-destructive)]" role="alert">
262
+ {errorText}
263
+ </p>
264
+ )}
265
+ {!errorText && helperText && (
266
+ <p id={\`\${id}-helper\`} className="text-xs text-[var(--bt-text-muted)]">
267
+ {helperText}
268
+ </p>
269
+ )}
270
+ </div>
271
+ );
272
+ }
273
+ );
274
+
275
+ Input.displayName = "Input";
276
+ `
277
+ },
278
+ {
279
+ name: "input.types.ts",
280
+ content: `import type { InputHTMLAttributes, ReactNode } from "react";
281
+ import type { BtTheme } from "../../types";
282
+
283
+ export type InputState = "default" | "error" | "disabled";
284
+
285
+ export interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "size"> {
286
+ theme?: BtTheme;
287
+ label?: string;
288
+ helperText?: string;
289
+ errorText?: string;
290
+ leftAddon?: ReactNode;
291
+ rightAddon?: ReactNode;
292
+ state?: InputState;
293
+ }
294
+ `
295
+ }
296
+ ]
297
+ };
298
+
299
+ // src/registry/components/label.ts
300
+ var label = {
301
+ name: "label",
302
+ category: "primitives",
303
+ description: "Styled form label with required indicator",
304
+ infraDeps: ["tokens", "globals", "utils"],
305
+ componentDeps: [],
306
+ npmDeps: [],
307
+ files: [
308
+ {
309
+ name: "Label.tsx",
310
+ content: `import { cn } from "@/lib/utils";
311
+ import type { LabelProps } from "./label.types";
312
+
313
+ export function Label({
314
+ theme,
315
+ required,
316
+ className,
317
+ children,
318
+ ...props
319
+ }: LabelProps) {
320
+ return (
321
+ <label
322
+ data-bt-theme={theme}
323
+ className={cn(
324
+ "text-sm font-medium text-[var(--bt-text-primary)] leading-none",
325
+ "peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
326
+ className
327
+ )}
328
+ {...props}
329
+ >
330
+ {children}
331
+ {required && (
332
+ <span className="ml-1 text-[var(--bt-destructive)]" aria-hidden="true">
333
+ *
334
+ </span>
335
+ )}
336
+ </label>
337
+ );
338
+ }
339
+ `
340
+ },
341
+ {
342
+ name: "label.types.ts",
343
+ content: `import type { LabelHTMLAttributes } from "react";
344
+ import type { BtTheme } from "../../types";
345
+
346
+ export interface LabelProps extends LabelHTMLAttributes<HTMLLabelElement> {
347
+ theme?: BtTheme;
348
+ required?: boolean;
349
+ }
350
+ `
351
+ }
352
+ ]
353
+ };
354
+
355
+ // src/registry/components/checkbox.ts
356
+ var checkbox = {
357
+ name: "checkbox",
358
+ category: "primitives",
359
+ description: "Accessible checkbox with indeterminate state",
360
+ infraDeps: ["tokens", "globals", "provider", "utils"],
361
+ componentDeps: [],
362
+ npmDeps: [],
363
+ files: [
364
+ {
365
+ name: "Checkbox.tsx",
366
+ content: `"use client";
367
+
368
+ import { useState, useCallback, useId } from "react";
369
+ import { cn } from "@/lib/utils";
370
+ import { useBakeThereTheme } from "../../provider";
371
+ import type { CheckboxProps } from "./checkbox.types";
372
+
373
+ const sizeClasses = {
374
+ sm: "h-4 w-4",
375
+ md: "h-5 w-5",
376
+ lg: "h-6 w-6",
377
+ };
378
+
379
+ const checkSizeClasses = {
380
+ sm: "h-2.5 w-2.5",
381
+ md: "h-3 w-3",
382
+ lg: "h-4 w-4",
383
+ };
384
+
385
+ export function Checkbox({
386
+ checked: controlledChecked,
387
+ defaultChecked = false,
388
+ onCheckedChange,
389
+ disabled = false,
390
+ size = "md",
391
+ theme,
392
+ className,
393
+ id: externalId,
394
+ "aria-label": ariaLabel,
395
+ "aria-labelledby": ariaLabelledby,
396
+ }: CheckboxProps) {
397
+ const { theme: contextTheme } = useBakeThereTheme();
398
+ const activeTheme = theme ?? contextTheme;
399
+ const generatedId = useId();
400
+ const id = externalId ?? generatedId;
401
+
402
+ const [internalChecked, setInternalChecked] = useState(defaultChecked);
403
+ const isChecked = controlledChecked !== undefined ? controlledChecked : internalChecked;
404
+
405
+ const toggle = useCallback(() => {
406
+ if (disabled) return;
407
+ const next = !isChecked;
408
+ if (controlledChecked === undefined) setInternalChecked(next);
409
+ onCheckedChange?.(next);
410
+ }, [disabled, isChecked, controlledChecked, onCheckedChange]);
411
+
412
+ const handleKeyDown = useCallback(
413
+ (e: React.KeyboardEvent) => {
414
+ if (e.key === " ") {
415
+ e.preventDefault();
416
+ toggle();
417
+ }
418
+ },
419
+ [toggle]
420
+ );
421
+
422
+ return (
423
+ <div
424
+ id={id}
425
+ data-bt-theme={activeTheme}
426
+ role="checkbox"
427
+ aria-checked={isChecked}
428
+ aria-disabled={disabled || undefined}
429
+ aria-label={ariaLabel}
430
+ aria-labelledby={ariaLabelledby}
431
+ tabIndex={disabled ? -1 : 0}
432
+ onClick={toggle}
433
+ onKeyDown={handleKeyDown}
434
+ className={cn(
435
+ "inline-flex items-center justify-center rounded-[var(--bt-radius-sm)] border",
436
+ "cursor-pointer transition-colors select-none",
437
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--bt-ring)]",
438
+ sizeClasses[size],
439
+ isChecked
440
+ ? "bg-[var(--bt-accent)] border-[var(--bt-accent)]"
441
+ : "bg-[var(--bt-bg-surface)] border-[var(--bt-border)]",
442
+ disabled && "opacity-[var(--bt-disabled-opacity)] pointer-events-none",
443
+ className
444
+ )}
445
+ >
446
+ {isChecked && (
447
+ <svg
448
+ className={cn("text-[var(--bt-text-inverse)]", checkSizeClasses[size])}
449
+ viewBox="0 0 12 12"
450
+ fill="none"
451
+ xmlns="http://www.w3.org/2000/svg"
452
+ aria-hidden="true"
453
+ >
454
+ <path
455
+ d="M2 6L5 9L10 3"
456
+ stroke="currentColor"
457
+ strokeWidth="1.5"
458
+ strokeLinecap="round"
459
+ strokeLinejoin="round"
460
+ />
461
+ </svg>
462
+ )}
463
+ </div>
464
+ );
465
+ }
466
+ `
467
+ },
468
+ {
469
+ name: "checkbox.types.ts",
470
+ content: `import type { BtTheme } from "../../types";
471
+
472
+ export type CheckboxSize = "sm" | "md" | "lg";
473
+
474
+ export interface CheckboxProps {
475
+ checked?: boolean;
476
+ defaultChecked?: boolean;
477
+ onCheckedChange?: (checked: boolean) => void;
478
+ disabled?: boolean;
479
+ size?: CheckboxSize;
480
+ theme?: BtTheme;
481
+ className?: string;
482
+ id?: string;
483
+ "aria-label"?: string;
484
+ "aria-labelledby"?: string;
485
+ }
486
+ `
487
+ }
488
+ ]
489
+ };
490
+
491
+ // src/registry/components/toggle.ts
492
+ var toggle = {
493
+ name: "toggle",
494
+ category: "primitives",
495
+ description: "Toggle switch for binary on/off state",
496
+ infraDeps: ["tokens", "globals", "provider", "utils"],
497
+ componentDeps: [],
498
+ npmDeps: [],
499
+ files: [
500
+ {
501
+ name: "Toggle.tsx",
502
+ content: `"use client";
503
+
504
+ import { useState, useCallback, useId } from "react";
505
+ import { cn } from "@/lib/utils";
506
+ import { useBakeThereTheme } from "../../provider";
507
+ import type { ToggleProps } from "./toggle.types";
508
+
509
+ const trackSizes = {
510
+ sm: "h-5 w-9",
511
+ md: "h-6 w-11",
512
+ lg: "h-7 w-14",
513
+ };
514
+
515
+ const thumbSizes = {
516
+ sm: "h-3.5 w-3.5",
517
+ md: "h-[18px] w-[18px]",
518
+ lg: "h-[22px] w-[22px]",
519
+ };
520
+
521
+ const thumbTranslate = {
522
+ sm: "translate-x-4",
523
+ md: "translate-x-5",
524
+ lg: "translate-x-7",
525
+ };
526
+
527
+ export function Toggle({
528
+ checked: controlledChecked,
529
+ defaultChecked = false,
530
+ onCheckedChange,
531
+ disabled = false,
532
+ size = "md",
533
+ theme,
534
+ className,
535
+ id: externalId,
536
+ "aria-label": ariaLabel,
537
+ "aria-labelledby": ariaLabelledby,
538
+ }: ToggleProps) {
539
+ const { theme: contextTheme } = useBakeThereTheme();
540
+ const activeTheme = theme ?? contextTheme;
541
+ const generatedId = useId();
542
+ const id = externalId ?? generatedId;
543
+
544
+ const [internalChecked, setInternalChecked] = useState(defaultChecked);
545
+ const isChecked = controlledChecked !== undefined ? controlledChecked : internalChecked;
546
+
547
+ const toggle = useCallback(() => {
548
+ if (disabled) return;
549
+ const next = !isChecked;
550
+ if (controlledChecked === undefined) setInternalChecked(next);
551
+ onCheckedChange?.(next);
552
+ }, [disabled, isChecked, controlledChecked, onCheckedChange]);
553
+
554
+ const handleKeyDown = useCallback(
555
+ (e: React.KeyboardEvent) => {
556
+ if (e.key === " " || e.key === "Enter") {
557
+ e.preventDefault();
558
+ toggle();
559
+ }
560
+ },
561
+ [toggle]
562
+ );
563
+
564
+ return (
565
+ <div
566
+ id={id}
567
+ data-bt-theme={activeTheme}
568
+ role="switch"
569
+ aria-checked={isChecked}
570
+ aria-disabled={disabled || undefined}
571
+ aria-label={ariaLabel}
572
+ aria-labelledby={ariaLabelledby}
573
+ tabIndex={disabled ? -1 : 0}
574
+ onClick={toggle}
575
+ onKeyDown={handleKeyDown}
576
+ className={cn(
577
+ "relative inline-flex items-center rounded-[var(--bt-radius-full)] cursor-pointer",
578
+ "transition-colors duration-200 select-none",
579
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--bt-ring)]",
580
+ trackSizes[size],
581
+ isChecked
582
+ ? "bg-[var(--bt-accent)]"
583
+ : "bg-[var(--bt-bg-muted)] border border-[var(--bt-border)]",
584
+ disabled && "opacity-[var(--bt-disabled-opacity)] pointer-events-none",
585
+ className
586
+ )}
587
+ >
588
+ <span
589
+ aria-hidden="true"
590
+ className={cn(
591
+ "absolute top-1/2 -translate-y-1/2 left-0.5",
592
+ "rounded-full bg-white shadow-sm",
593
+ "transition-transform duration-200",
594
+ thumbSizes[size],
595
+ isChecked ? thumbTranslate[size] : "translate-x-0"
596
+ )}
597
+ />
598
+ </div>
599
+ );
600
+ }
601
+ `
602
+ },
603
+ {
604
+ name: "toggle.types.ts",
605
+ content: `import type { BtTheme } from "../../types";
606
+
607
+ export type ToggleSize = "sm" | "md" | "lg";
608
+
609
+ export interface ToggleProps {
610
+ checked?: boolean;
611
+ defaultChecked?: boolean;
612
+ onCheckedChange?: (checked: boolean) => void;
613
+ disabled?: boolean;
614
+ size?: ToggleSize;
615
+ theme?: BtTheme;
616
+ className?: string;
617
+ id?: string;
618
+ "aria-label"?: string;
619
+ "aria-labelledby"?: string;
620
+ }
621
+ `
622
+ }
623
+ ]
624
+ };
625
+
626
+ // src/registry/components/text.ts
627
+ var text = {
628
+ name: "text",
629
+ category: "primitives",
630
+ description: "Typography primitives \u2014 heading, body, caption, code",
631
+ infraDeps: ["tokens", "globals", "utils"],
632
+ componentDeps: [],
633
+ npmDeps: [],
634
+ files: [
635
+ {
636
+ name: "Text.tsx",
637
+ content: `import type { ElementType, JSX } from "react";
638
+ import { cn } from "@/lib/utils";
639
+ import type { TextVariant, TextFont, TextProps } from "./text.types";
640
+
641
+ const elementMap: Record<TextVariant, keyof JSX.IntrinsicElements> = {
642
+ "display-lg": "h1",
643
+ "display": "h1",
644
+ "h1": "h1",
645
+ "h2": "h2",
646
+ "h3": "h3",
647
+ "h4": "h4",
648
+ "h5": "h5",
649
+ "h6": "h6",
650
+ "body-lg": "p",
651
+ "body": "p",
652
+ "body-sm": "p",
653
+ "caption": "span",
654
+ "overline": "span",
655
+ "code": "code",
656
+ };
657
+
658
+ const sizeMap: Record<TextVariant, string> = {
659
+ "display-lg": "clamp(3rem, 6vw + 1rem, 6rem)",
660
+ "display": "clamp(2.25rem, 4vw + 1rem, 4.5rem)",
661
+ "h1": "clamp(1.875rem, 3vw + 1rem, 3rem)",
662
+ "h2": "clamp(1.5rem, 2vw + 1rem, 2.25rem)",
663
+ "h3": "clamp(1.25rem, 1.5vw + 0.75rem, 1.875rem)",
664
+ "h4": "clamp(1.125rem, 1vw + 0.75rem, 1.5rem)",
665
+ "h5": "clamp(1rem, 0.5vw + 0.75rem, 1.25rem)",
666
+ "h6": "clamp(0.875rem, 0.5vw + 0.625rem, 1rem)",
667
+ "body-lg": "clamp(1rem, 0.5vw + 0.75rem, 1.25rem)",
668
+ "body": "1rem",
669
+ "body-sm": "0.875rem",
670
+ "caption": "0.75rem",
671
+ "overline": "0.6875rem",
672
+ "code": "0.875rem",
673
+ };
674
+
675
+ const weightMap: Record<TextVariant, number> = {
676
+ "display-lg": 800,
677
+ "display": 700,
678
+ "h1": 700,
679
+ "h2": 600,
680
+ "h3": 600,
681
+ "h4": 600,
682
+ "h5": 500,
683
+ "h6": 500,
684
+ "body-lg": 400,
685
+ "body": 400,
686
+ "body-sm": 400,
687
+ "caption": 400,
688
+ "overline": 600,
689
+ "code": 400,
690
+ };
691
+
692
+ const lineHeightMap: Record<TextVariant, string> = {
693
+ "display-lg": "1.1",
694
+ "display": "1.1",
695
+ "h1": "1.2",
696
+ "h2": "1.25",
697
+ "h3": "1.3",
698
+ "h4": "1.35",
699
+ "h5": "1.4",
700
+ "h6": "1.4",
701
+ "body-lg": "1.7",
702
+ "body": "1.6",
703
+ "body-sm": "1.6",
704
+ "caption": "1.5",
705
+ "overline": "1.4",
706
+ "code": "1.5",
707
+ };
708
+
709
+ const fontStacks: Record<TextFont, string> = {
710
+ sans: "var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif",
711
+ serif: "Georgia, 'Times New Roman', ui-serif, serif",
712
+ mono: "var(--font-geist-mono), ui-monospace, monospace",
713
+ };
714
+
715
+ export function Text({
716
+ variant = "body",
717
+ font,
718
+ as,
719
+ theme,
720
+ style,
721
+ className,
722
+ children,
723
+ ...props
724
+ }: TextProps) {
725
+ const Tag = (as ?? elementMap[variant]) as ElementType;
726
+ const isCode = variant === "code";
727
+ const resolvedFont = isCode
728
+ ? fontStacks.mono
729
+ : font
730
+ ? fontStacks[font]
731
+ : undefined;
732
+
733
+ return (
734
+ <Tag
735
+ {...(theme ? { "data-bt-theme": theme } : {})}
736
+ style={{
737
+ fontSize: sizeMap[variant],
738
+ fontWeight: weightMap[variant],
739
+ lineHeight: lineHeightMap[variant],
740
+ ...(resolvedFont ? { fontFamily: resolvedFont } : {}),
741
+ ...(variant === "overline"
742
+ ? { textTransform: "uppercase" as const, letterSpacing: "0.1em" }
743
+ : {}),
744
+ ...style,
745
+ }}
746
+ className={cn("text-[var(--bt-text-primary)]", className)}
747
+ {...props}
748
+ >
749
+ {children}
750
+ </Tag>
751
+ );
752
+ }
753
+ `
754
+ },
755
+ {
756
+ name: "text.types.ts",
757
+ content: `import type { HTMLAttributes, JSX } from "react";
758
+ import type { BtTheme } from "../../types";
759
+
760
+ export type TextVariant =
761
+ | "display-lg"
762
+ | "display"
763
+ | "h1"
764
+ | "h2"
765
+ | "h3"
766
+ | "h4"
767
+ | "h5"
768
+ | "h6"
769
+ | "body-lg"
770
+ | "body"
771
+ | "body-sm"
772
+ | "caption"
773
+ | "overline"
774
+ | "code";
775
+
776
+ export type TextFont = "sans" | "serif" | "mono";
777
+
778
+ export interface TextProps extends HTMLAttributes<HTMLElement> {
779
+ variant?: TextVariant;
780
+ font?: TextFont;
781
+ as?: keyof JSX.IntrinsicElements;
782
+ theme?: BtTheme;
783
+ }
784
+ `
785
+ }
786
+ ]
787
+ };
788
+
789
+ // src/registry/components/textarea.ts
790
+ var textarea = {
791
+ name: "textarea",
792
+ category: "primitives",
793
+ description: "Multi-line text input with auto-resize support",
794
+ infraDeps: ["tokens", "globals", "provider", "utils"],
795
+ componentDeps: [],
796
+ npmDeps: [],
797
+ files: [
798
+ {
799
+ name: "Textarea.tsx",
800
+ content: `"use client";
801
+
802
+ import { forwardRef, useCallback } from "react";
803
+ import { cn } from "@/lib/utils";
804
+ import { useBakeThereTheme } from "../../provider";
805
+ import type { TextareaProps } from "./textarea.types";
806
+
807
+ export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
808
+ (
809
+ {
810
+ theme,
811
+ autoResize = false,
812
+ maxRows,
813
+ disabled,
814
+ className,
815
+ onInput,
816
+ rows = 3,
817
+ style,
818
+ ...props
819
+ },
820
+ ref
821
+ ) => {
822
+ const { theme: contextTheme } = useBakeThereTheme();
823
+ const activeTheme = theme ?? contextTheme;
824
+
825
+ // Fallback for 'line-height: normal' \u2014 approximates text-sm * 1.5
826
+ const LINE_HEIGHT_FALLBACK = 21;
827
+
828
+ const doResize = useCallback(
829
+ (el: HTMLTextAreaElement) => {
830
+ el.style.height = "auto";
831
+ const lineHeight =
832
+ parseInt(getComputedStyle(el).lineHeight) || LINE_HEIGHT_FALLBACK;
833
+ const newHeight = maxRows
834
+ ? Math.min(el.scrollHeight, maxRows * lineHeight)
835
+ : el.scrollHeight;
836
+ el.style.height = \`\${newHeight}px\`;
837
+ },
838
+ [maxRows]
839
+ );
840
+
841
+ return (
842
+ <div data-bt-theme={activeTheme} className="w-full">
843
+ <textarea
844
+ ref={ref}
845
+ rows={rows}
846
+ disabled={disabled}
847
+ onInput={(e) => {
848
+ if (autoResize) doResize(e.currentTarget as HTMLTextAreaElement);
849
+ onInput?.(e);
850
+ }}
851
+ style={
852
+ autoResize
853
+ ? { resize: "none", overflow: "hidden", ...style }
854
+ : style
855
+ }
856
+ className={cn(
857
+ "w-full rounded-[var(--bt-radius-md)] border bg-[var(--bt-bg-surface)]",
858
+ "text-[var(--bt-text-primary)] placeholder:text-[var(--bt-text-muted)]",
859
+ "px-3 py-2 text-sm transition-colors",
860
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--bt-ring)]",
861
+ "focus-visible:border-[var(--bt-border-focus)] border-[var(--bt-border)]",
862
+ "disabled:bg-[var(--bt-bg-muted)] disabled:text-[var(--bt-text-muted)] disabled:border-[var(--bt-border-muted)] disabled:pointer-events-none disabled:cursor-not-allowed",
863
+ !autoResize && "resize-y",
864
+ className
865
+ )}
866
+ {...props}
867
+ />
868
+ </div>
869
+ );
870
+ }
871
+ );
872
+
873
+ Textarea.displayName = "Textarea";
874
+ `
875
+ },
876
+ {
877
+ name: "textarea.types.ts",
878
+ content: `import type { TextareaHTMLAttributes } from "react";
879
+ import type { BtTheme } from "../../types";
880
+
881
+ export interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {
882
+ theme?: BtTheme;
883
+ autoResize?: boolean;
884
+ maxRows?: number;
885
+ }
886
+ `
887
+ }
888
+ ]
889
+ };
890
+
891
+ // src/registry/components/radio.ts
892
+ var radio = {
893
+ name: "radio",
894
+ category: "primitives",
895
+ description: "Radio button group for single selection",
896
+ infraDeps: ["tokens", "globals", "provider", "utils"],
897
+ componentDeps: [],
898
+ npmDeps: [],
899
+ files: [
900
+ {
901
+ name: "Radio.tsx",
902
+ content: `"use client";
903
+
904
+ import {
905
+ createContext,
906
+ useContext,
907
+ useId,
908
+ useState,
909
+ useCallback,
910
+ useRef,
911
+ type KeyboardEvent,
912
+ } from "react";
913
+ import { cn } from "@/lib/utils";
914
+ import { useBakeThereTheme } from "../../provider";
915
+ import type { RadioGroupProps, RadioGroupItemProps } from "./radio.types";
916
+
917
+ interface RadioContextValue {
918
+ value: string;
919
+ setSelected: (value: string) => void;
920
+ name: string;
921
+ groupDisabled: boolean;
922
+ }
923
+
924
+ const RadioContext = createContext<RadioContextValue | null>(null);
925
+
926
+ function useRadioContext() {
927
+ const ctx = useContext(RadioContext);
928
+ if (!ctx) throw new Error("RadioGroupItem must be used inside <RadioGroup>");
929
+ return ctx;
930
+ }
931
+
932
+ export function RadioGroup({
933
+ value: controlledValue,
934
+ defaultValue = "",
935
+ onValueChange,
936
+ name,
937
+ disabled = false,
938
+ theme,
939
+ className,
940
+ children,
941
+ }: RadioGroupProps) {
942
+ const { theme: contextTheme } = useBakeThereTheme();
943
+ const activeTheme = theme ?? contextTheme;
944
+ const groupRef = useRef<HTMLDivElement>(null);
945
+
946
+ const [internalValue, setInternalValue] = useState(defaultValue);
947
+ const value = controlledValue ?? internalValue;
948
+
949
+ const generatedName = useId();
950
+ const groupName = name ?? generatedName;
951
+
952
+ const setSelected = useCallback(
953
+ (val: string) => {
954
+ if (controlledValue === undefined) setInternalValue(val);
955
+ onValueChange?.(val);
956
+ },
957
+ [controlledValue, onValueChange]
958
+ );
959
+
960
+ const handleKeyDown = useCallback(
961
+ (e: KeyboardEvent<HTMLDivElement>) => {
962
+ const inputs = groupRef.current?.querySelectorAll<HTMLInputElement>(
963
+ 'input[type="radio"]:not(:disabled)'
964
+ );
965
+ if (!inputs || inputs.length === 0) return;
966
+ const arr = Array.from(inputs);
967
+ const idx = arr.findIndex((el) => el.value === value);
968
+
969
+ if (e.key === "ArrowDown" || e.key === "ArrowRight") {
970
+ e.preventDefault();
971
+ const next = arr[(idx + 1) % arr.length];
972
+ next.focus();
973
+ setSelected(next.value);
974
+ } else if (e.key === "ArrowUp" || e.key === "ArrowLeft") {
975
+ e.preventDefault();
976
+ const prev = arr[(idx - 1 + arr.length) % arr.length];
977
+ prev.focus();
978
+ setSelected(prev.value);
979
+ }
980
+ },
981
+ [value, setSelected]
982
+ );
983
+
984
+ return (
985
+ <RadioContext.Provider
986
+ value={{ value, setSelected, name: groupName, groupDisabled: disabled }}
987
+ >
988
+ <div
989
+ ref={groupRef}
990
+ data-bt-theme={activeTheme}
991
+ role="radiogroup"
992
+ onKeyDown={handleKeyDown}
993
+ className={cn("flex flex-col gap-2", className)}
994
+ >
995
+ {children}
996
+ </div>
997
+ </RadioContext.Provider>
998
+ );
999
+ }
1000
+
1001
+ export function RadioGroupItem({
1002
+ value,
1003
+ label,
1004
+ disabled: itemDisabled = false,
1005
+ className,
1006
+ }: RadioGroupItemProps) {
1007
+ const { value: selectedValue, setSelected, name, groupDisabled } =
1008
+ useRadioContext();
1009
+ const isSelected = selectedValue === value;
1010
+ const isDisabled = groupDisabled || itemDisabled;
1011
+
1012
+ return (
1013
+ <label
1014
+ className={cn(
1015
+ "flex items-center gap-2 cursor-pointer select-none",
1016
+ isDisabled && "opacity-[var(--bt-disabled-opacity)] pointer-events-none",
1017
+ className
1018
+ )}
1019
+ >
1020
+ <input
1021
+ type="radio"
1022
+ name={name}
1023
+ value={value}
1024
+ checked={isSelected}
1025
+ disabled={isDisabled}
1026
+ onChange={() => setSelected(value)}
1027
+ className="sr-only peer"
1028
+ />
1029
+ <div
1030
+ aria-hidden="true"
1031
+ className={cn(
1032
+ "h-4 w-4 rounded-full border-2 flex items-center justify-center",
1033
+ "transition-colors",
1034
+ "peer-focus-visible:ring-2 peer-focus-visible:ring-[var(--bt-ring)] peer-focus-visible:ring-offset-1",
1035
+ isSelected
1036
+ ? "border-[var(--bt-accent)]"
1037
+ : "border-[var(--bt-border)]"
1038
+ )}
1039
+ >
1040
+ {isSelected && (
1041
+ <div className="h-2 w-2 rounded-full bg-[var(--bt-accent)]" />
1042
+ )}
1043
+ </div>
1044
+ <span className="text-sm text-[var(--bt-text-primary)]">{label}</span>
1045
+ </label>
1046
+ );
1047
+ }
1048
+ `
1049
+ },
1050
+ {
1051
+ name: "radio.types.ts",
1052
+ content: `import type { ReactNode } from "react";
1053
+ import type { BtTheme } from "../../types";
1054
+
1055
+ export interface RadioGroupProps {
1056
+ value?: string;
1057
+ defaultValue?: string;
1058
+ onValueChange?: (value: string) => void;
1059
+ name?: string;
1060
+ disabled?: boolean;
1061
+ theme?: BtTheme;
1062
+ className?: string;
1063
+ children?: ReactNode;
1064
+ }
1065
+
1066
+ export interface RadioGroupItemProps {
1067
+ value: string;
1068
+ label: string;
1069
+ disabled?: boolean;
1070
+ className?: string;
1071
+ }
1072
+ `
1073
+ }
1074
+ ]
1075
+ };
1076
+
1077
+ // src/registry/components/slider.ts
1078
+ var slider = {
1079
+ name: "slider",
1080
+ category: "primitives",
1081
+ description: "Range slider with themed track and thumb",
1082
+ infraDeps: ["tokens", "globals", "provider", "utils"],
1083
+ componentDeps: [],
1084
+ npmDeps: [],
1085
+ files: [
1086
+ {
1087
+ name: "Slider.tsx",
1088
+ content: `"use client";
1089
+
1090
+ import { useState, useCallback } from "react";
1091
+ import { cn } from "@/lib/utils";
1092
+ import { useBakeThereTheme } from "../../provider";
1093
+ import type { SliderProps } from "./slider.types";
1094
+
1095
+ export function Slider({
1096
+ value: controlledValue,
1097
+ defaultValue = 50,
1098
+ min = 0,
1099
+ max = 100,
1100
+ step = 1,
1101
+ disabled = false,
1102
+ onChange,
1103
+ theme,
1104
+ className,
1105
+ }: SliderProps) {
1106
+ const { theme: contextTheme } = useBakeThereTheme();
1107
+ const activeTheme = theme ?? contextTheme;
1108
+
1109
+ const [internalValue, setInternalValue] = useState(defaultValue);
1110
+ const value = controlledValue ?? internalValue;
1111
+
1112
+ const range = max - min;
1113
+ const percentage = range === 0 ? 0 : ((value - min) / range) * 100;
1114
+
1115
+ const handleChange = useCallback(
1116
+ (e: React.ChangeEvent<HTMLInputElement>) => {
1117
+ const next = Number(e.target.value);
1118
+ if (controlledValue === undefined) setInternalValue(next);
1119
+ onChange?.(next);
1120
+ },
1121
+ [controlledValue, onChange]
1122
+ );
1123
+
1124
+ return (
1125
+ <div data-bt-theme={activeTheme} className={cn("w-full", className)}>
1126
+ <input
1127
+ type="range"
1128
+ min={min}
1129
+ max={max}
1130
+ step={step}
1131
+ value={value}
1132
+ disabled={disabled}
1133
+ onChange={handleChange}
1134
+ style={
1135
+ { "--bt-slider-value": \`\${percentage}%\` } as React.CSSProperties
1136
+ }
1137
+ className="bt-slider"
1138
+ />
1139
+ </div>
1140
+ );
1141
+ }
1142
+ `
1143
+ },
1144
+ {
1145
+ name: "slider.types.ts",
1146
+ content: `import type { BtTheme } from "../../types";
1147
+
1148
+ export interface SliderProps {
1149
+ value?: number;
1150
+ defaultValue?: number;
1151
+ min?: number;
1152
+ max?: number;
1153
+ step?: number;
1154
+ disabled?: boolean;
1155
+ onChange?: (value: number) => void;
1156
+ theme?: BtTheme;
1157
+ className?: string;
1158
+ }
1159
+ `
1160
+ }
1161
+ ]
1162
+ };
1163
+
1164
+ // src/registry/components/select.ts
1165
+ var select = {
1166
+ name: "select",
1167
+ category: "primitives",
1168
+ description: "Custom select dropdown with keyboard navigation",
1169
+ infraDeps: ["tokens", "globals", "provider", "utils"],
1170
+ componentDeps: [],
1171
+ npmDeps: [],
1172
+ files: [
1173
+ {
1174
+ name: "Select.tsx",
1175
+ content: `"use client";
1176
+
1177
+ import {
1178
+ createContext,
1179
+ useContext,
1180
+ useState,
1181
+ useRef,
1182
+ useEffect,
1183
+ useLayoutEffect,
1184
+ useCallback,
1185
+ useId,
1186
+ type ReactNode,
1187
+ } from "react";
1188
+ import { cn } from "@/lib/utils";
1189
+ import { useBakeThereTheme } from "../../provider";
1190
+ import type {
1191
+ SelectProps,
1192
+ SelectTriggerProps,
1193
+ SelectContentProps,
1194
+ SelectItemProps,
1195
+ SelectGroupProps,
1196
+ SelectLabelProps,
1197
+ } from "./select.types";
1198
+
1199
+ interface SelectItem {
1200
+ value: string;
1201
+ label: string;
1202
+ }
1203
+
1204
+ interface SelectContextValue {
1205
+ value: string | undefined;
1206
+ selectedLabel: string | undefined;
1207
+ onSelect: (value: string, label: string) => void;
1208
+ open: boolean;
1209
+ onOpenChange: (open: boolean) => void;
1210
+ triggerRef: React.RefObject<HTMLButtonElement | null>;
1211
+ contentId: string;
1212
+ highlightedIndex: number | null;
1213
+ setHighlightedIndex: (i: number | null) => void;
1214
+ items: SelectItem[];
1215
+ registerItem: (value: string, label: string) => void;
1216
+ unregisterItem: (value: string) => void;
1217
+ disabled: boolean;
1218
+ }
1219
+
1220
+ const SelectContext = createContext<SelectContextValue | null>(null);
1221
+
1222
+ function useSelectContext() {
1223
+ const ctx = useContext(SelectContext);
1224
+ if (!ctx) throw new Error("Select sub-components must be used inside <Select>");
1225
+ return ctx;
1226
+ }
1227
+
1228
+ export function Select({
1229
+ value: controlledValue,
1230
+ onValueChange,
1231
+ defaultValue,
1232
+ disabled = false,
1233
+ theme,
1234
+ children,
1235
+ }: SelectProps) {
1236
+ const { theme: contextTheme } = useBakeThereTheme();
1237
+ const activeTheme = theme ?? contextTheme;
1238
+ const [internalValue, setInternalValue] = useState<string | undefined>(defaultValue);
1239
+ const value = controlledValue !== undefined ? controlledValue : internalValue;
1240
+ const [selectedLabel, setSelectedLabel] = useState<string | undefined>(undefined);
1241
+ const [open, setOpen] = useState(false);
1242
+ const [highlightedIndex, setHighlightedIndex] = useState<number | null>(null);
1243
+ const [items, setItems] = useState<SelectItem[]>([]);
1244
+ const triggerRef = useRef<HTMLButtonElement | null>(null);
1245
+ const contentId = useId();
1246
+
1247
+ const handleSelect = useCallback(
1248
+ (val: string, label: string) => {
1249
+ if (controlledValue === undefined) setInternalValue(val);
1250
+ setSelectedLabel(label);
1251
+ onValueChange?.(val);
1252
+ setOpen(false);
1253
+ setHighlightedIndex(null);
1254
+ setTimeout(() => triggerRef.current?.focus(), 0);
1255
+ },
1256
+ [controlledValue, onValueChange]
1257
+ );
1258
+
1259
+ const handleOpenChange = useCallback(
1260
+ (val: boolean) => {
1261
+ setOpen(val);
1262
+ if (val) {
1263
+ const idx = items.findIndex((i) => i.value === value);
1264
+ setHighlightedIndex(idx >= 0 ? idx : null);
1265
+ } else {
1266
+ setHighlightedIndex(null);
1267
+ }
1268
+ },
1269
+ [items, value]
1270
+ );
1271
+
1272
+ const registerItem = useCallback((itemValue: string, label: string) => {
1273
+ setItems((prev) => [...prev, { value: itemValue, label }]);
1274
+ }, []);
1275
+
1276
+ const unregisterItem = useCallback((itemValue: string) => {
1277
+ setItems((prev) => prev.filter((i) => i.value !== itemValue));
1278
+ }, []);
1279
+
1280
+ // Close on outside click
1281
+ useEffect(() => {
1282
+ if (!open) return;
1283
+ function handleMouseDown(e: MouseEvent) {
1284
+ const content = document.getElementById(contentId);
1285
+ if (
1286
+ triggerRef.current &&
1287
+ !triggerRef.current.contains(e.target as Node) &&
1288
+ !content?.contains(e.target as Node)
1289
+ ) {
1290
+ setOpen(false);
1291
+ setHighlightedIndex(null);
1292
+ }
1293
+ }
1294
+ document.addEventListener("mousedown", handleMouseDown);
1295
+ return () => document.removeEventListener("mousedown", handleMouseDown);
1296
+ }, [open, contentId]);
1297
+
1298
+ return (
1299
+ <SelectContext.Provider
1300
+ value={{
1301
+ value,
1302
+ selectedLabel,
1303
+ onSelect: handleSelect,
1304
+ open,
1305
+ onOpenChange: handleOpenChange,
1306
+ triggerRef,
1307
+ contentId,
1308
+ highlightedIndex,
1309
+ setHighlightedIndex,
1310
+ items,
1311
+ registerItem,
1312
+ unregisterItem,
1313
+ disabled,
1314
+ }}
1315
+ >
1316
+ <div data-bt-theme={activeTheme}>
1317
+ {children}
1318
+ </div>
1319
+ </SelectContext.Provider>
1320
+ );
1321
+ }
1322
+
1323
+ export function SelectTrigger({ placeholder = "Select...", className, ...props }: SelectTriggerProps) {
1324
+ const {
1325
+ selectedLabel,
1326
+ onSelect,
1327
+ open,
1328
+ onOpenChange,
1329
+ triggerRef,
1330
+ contentId,
1331
+ highlightedIndex,
1332
+ setHighlightedIndex,
1333
+ items,
1334
+ disabled,
1335
+ } = useSelectContext();
1336
+
1337
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLButtonElement>) => {
1338
+ if (disabled) return;
1339
+ const count = items.length;
1340
+
1341
+ if (!open) {
1342
+ if (
1343
+ e.key === " " ||
1344
+ e.key === "Enter" ||
1345
+ e.key === "ArrowDown" ||
1346
+ e.key === "ArrowUp"
1347
+ ) {
1348
+ e.preventDefault();
1349
+ onOpenChange(true);
1350
+ }
1351
+ return;
1352
+ }
1353
+
1354
+ if (e.key === "ArrowDown") {
1355
+ e.preventDefault();
1356
+ setHighlightedIndex(
1357
+ highlightedIndex === null ? 0 : (highlightedIndex + 1) % count
1358
+ );
1359
+ } else if (e.key === "ArrowUp") {
1360
+ e.preventDefault();
1361
+ setHighlightedIndex(
1362
+ highlightedIndex === null
1363
+ ? count - 1
1364
+ : (highlightedIndex - 1 + count) % count
1365
+ );
1366
+ } else if (e.key === "Enter" || e.key === " ") {
1367
+ e.preventDefault();
1368
+ if (highlightedIndex !== null && items[highlightedIndex]) {
1369
+ onSelect(items[highlightedIndex].value, items[highlightedIndex].label);
1370
+ }
1371
+ } else if (e.key === "Escape" || e.key === "Tab") {
1372
+ if (e.key === "Tab") e.preventDefault();
1373
+ onOpenChange(false);
1374
+ triggerRef.current?.focus();
1375
+ }
1376
+ };
1377
+
1378
+ return (
1379
+ <button
1380
+ ref={triggerRef}
1381
+ type="button"
1382
+ aria-haspopup="listbox"
1383
+ aria-expanded={open}
1384
+ aria-controls={contentId}
1385
+ disabled={disabled}
1386
+ onKeyDown={handleKeyDown}
1387
+ onClick={() => onOpenChange(!open)}
1388
+ className={cn(
1389
+ "h-10 px-3 text-sm border border-[var(--bt-border)] rounded-[var(--bt-radius-md)]",
1390
+ "bg-[var(--bt-bg-base)] text-[var(--bt-text-primary)] inline-flex items-center justify-between gap-2",
1391
+ "min-w-[120px]",
1392
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--bt-ring)]",
1393
+ "disabled:opacity-50 disabled:cursor-not-allowed",
1394
+ "transition-colors",
1395
+ className
1396
+ )}
1397
+ {...props}
1398
+ >
1399
+ <span
1400
+ className={cn(
1401
+ "truncate",
1402
+ selectedLabel
1403
+ ? "text-[var(--bt-text-primary)]"
1404
+ : "text-[var(--bt-text-muted)]"
1405
+ )}
1406
+ >
1407
+ {selectedLabel ?? placeholder}
1408
+ </span>
1409
+ <svg
1410
+ width="16"
1411
+ height="16"
1412
+ viewBox="0 0 24 24"
1413
+ fill="none"
1414
+ stroke="currentColor"
1415
+ strokeWidth="2"
1416
+ strokeLinecap="round"
1417
+ strokeLinejoin="round"
1418
+ aria-hidden="true"
1419
+ className={cn(
1420
+ "shrink-0 text-[var(--bt-text-muted)] transition-transform duration-200",
1421
+ open && "rotate-180"
1422
+ )}
1423
+ >
1424
+ <polyline points="6 9 12 15 18 9" />
1425
+ </svg>
1426
+ </button>
1427
+ );
1428
+ }
1429
+
1430
+ export function SelectContent({ children, className, style, ...props }: SelectContentProps) {
1431
+ const { open, triggerRef, contentId, highlightedIndex, items } = useSelectContext();
1432
+ const contentRef = useRef<HTMLDivElement>(null);
1433
+ const [pos, setPos] = useState<{ top: number; left: number; width: number } | null>(null);
1434
+
1435
+ const highlightedItemId =
1436
+ highlightedIndex !== null && items[highlightedIndex]
1437
+ ? \`select-item-\${items[highlightedIndex].value}\`
1438
+ : undefined;
1439
+
1440
+ useEffect(() => {
1441
+ if (!open) {
1442
+ setPos(null);
1443
+ return;
1444
+ }
1445
+
1446
+ function recalculate() {
1447
+ if (!triggerRef.current) return;
1448
+ const rect = triggerRef.current.getBoundingClientRect();
1449
+ setPos({ top: rect.bottom + 4, left: rect.left, width: rect.width });
1450
+ }
1451
+
1452
+ recalculate();
1453
+ window.addEventListener("scroll", recalculate, { passive: true, capture: true });
1454
+ window.addEventListener("resize", recalculate, { passive: true });
1455
+ return () => {
1456
+ window.removeEventListener("scroll", recalculate, { capture: true });
1457
+ window.removeEventListener("resize", recalculate);
1458
+ };
1459
+ }, [open]);
1460
+
1461
+ if (!open) return null;
1462
+
1463
+ return (
1464
+ <div
1465
+ ref={contentRef}
1466
+ id={contentId}
1467
+ role="listbox"
1468
+ aria-activedescendant={highlightedItemId}
1469
+ style={{
1470
+ position: "fixed",
1471
+ top: pos?.top ?? 0,
1472
+ left: pos?.left ?? 0,
1473
+ width: pos?.width ?? 0,
1474
+ visibility: pos ? "visible" : "hidden",
1475
+ pointerEvents: pos ? "auto" : "none",
1476
+ zIndex: 60,
1477
+ animation: "bt-fade-in 0.15s ease-out",
1478
+ ...style,
1479
+ }}
1480
+ className={cn(
1481
+ "bg-[var(--bt-bg-surface)] border border-[var(--bt-border)]",
1482
+ "shadow-[var(--bt-shadow-md)] rounded-[var(--bt-radius-md)] py-1",
1483
+ "max-h-[300px] overflow-y-auto",
1484
+ className
1485
+ )}
1486
+ {...props}
1487
+ >
1488
+ {children}
1489
+ </div>
1490
+ );
1491
+ }
1492
+
1493
+ export function SelectItem({
1494
+ value: itemValue,
1495
+ disabled = false,
1496
+ children,
1497
+ className,
1498
+ ...props
1499
+ }: SelectItemProps) {
1500
+ const {
1501
+ value,
1502
+ onSelect,
1503
+ registerItem,
1504
+ unregisterItem,
1505
+ highlightedIndex,
1506
+ setHighlightedIndex,
1507
+ items,
1508
+ } = useSelectContext();
1509
+
1510
+ const isSelected = value === itemValue;
1511
+ const itemIndex = items.findIndex((i) => i.value === itemValue);
1512
+ const isHighlighted = highlightedIndex !== null && itemIndex === highlightedIndex;
1513
+ const itemId = \`select-item-\${itemValue}\`;
1514
+
1515
+ useLayoutEffect(() => {
1516
+ if (disabled) return;
1517
+ const label =
1518
+ typeof children === "string" ? children : itemValue;
1519
+ registerItem(itemValue, label);
1520
+ return () => unregisterItem(itemValue);
1521
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1522
+ }, [itemValue, disabled]);
1523
+
1524
+ return (
1525
+ <div
1526
+ id={itemId}
1527
+ role="option"
1528
+ aria-selected={isSelected}
1529
+ aria-disabled={disabled || undefined}
1530
+ data-highlighted={isHighlighted ? "true" : undefined}
1531
+ tabIndex={-1}
1532
+ onClick={() => {
1533
+ if (!disabled) {
1534
+ const label = typeof children === "string" ? children : itemValue;
1535
+ onSelect(itemValue, label);
1536
+ }
1537
+ }}
1538
+ onMouseEnter={() => {
1539
+ if (!disabled && itemIndex >= 0) setHighlightedIndex(itemIndex);
1540
+ }}
1541
+ className={cn(
1542
+ "px-3 py-2 text-sm cursor-pointer flex items-center justify-between",
1543
+ "text-[var(--bt-text-primary)]",
1544
+ isHighlighted && "bg-[var(--bt-hover-bg)]",
1545
+ disabled && "opacity-50 cursor-not-allowed pointer-events-none",
1546
+ className
1547
+ )}
1548
+ {...props}
1549
+ >
1550
+ <span>{children}</span>
1551
+ {isSelected && (
1552
+ <svg
1553
+ width="16"
1554
+ height="16"
1555
+ viewBox="0 0 24 24"
1556
+ fill="none"
1557
+ stroke="currentColor"
1558
+ strokeWidth="2"
1559
+ strokeLinecap="round"
1560
+ strokeLinejoin="round"
1561
+ aria-hidden="true"
1562
+ className="shrink-0 text-[var(--bt-accent)]"
1563
+ >
1564
+ <polyline points="20 6 9 17 4 12" />
1565
+ </svg>
1566
+ )}
1567
+ </div>
1568
+ );
1569
+ }
1570
+
1571
+ export function SelectGroup({ className, children, ...props }: SelectGroupProps) {
1572
+ return (
1573
+ <div role="group" className={cn("py-1", className)} {...props}>
1574
+ {children}
1575
+ </div>
1576
+ );
1577
+ }
1578
+
1579
+ export function SelectLabel({ className, children, ...props }: SelectLabelProps) {
1580
+ return (
1581
+ <p
1582
+ className={cn(
1583
+ "px-3 py-1 text-xs font-semibold text-[var(--bt-text-muted)] uppercase tracking-wider",
1584
+ className
1585
+ )}
1586
+ {...props}
1587
+ >
1588
+ {children}
1589
+ </p>
1590
+ );
1591
+ }
1592
+ `
1593
+ },
1594
+ {
1595
+ name: "select.types.ts",
1596
+ content: `import type { HTMLAttributes, ButtonHTMLAttributes, ReactNode } from "react";
1597
+ import type { BtTheme } from "../../types";
1598
+
1599
+ export interface SelectProps {
1600
+ value?: string;
1601
+ onValueChange?: (value: string) => void;
1602
+ defaultValue?: string;
1603
+ disabled?: boolean;
1604
+ theme?: BtTheme;
1605
+ children: ReactNode;
1606
+ }
1607
+
1608
+ export interface SelectTriggerProps extends ButtonHTMLAttributes<HTMLButtonElement> {
1609
+ placeholder?: string;
1610
+ }
1611
+
1612
+ export type SelectContentProps = HTMLAttributes<HTMLDivElement>;
1613
+
1614
+ export interface SelectItemProps extends HTMLAttributes<HTMLDivElement> {
1615
+ value: string;
1616
+ disabled?: boolean;
1617
+ }
1618
+
1619
+ export type SelectGroupProps = HTMLAttributes<HTMLDivElement>;
1620
+
1621
+ export interface SelectLabelProps extends HTMLAttributes<HTMLParagraphElement> {}
1622
+ `
1623
+ }
1624
+ ]
1625
+ };
1626
+
1627
+ // src/registry/components/card.ts
1628
+ var card = {
1629
+ name: "card",
1630
+ category: "display",
1631
+ description: "Content container with header, content, and footer sections",
1632
+ infraDeps: ["tokens", "globals", "utils"],
1633
+ componentDeps: [],
1634
+ npmDeps: [],
1635
+ files: [
1636
+ {
1637
+ name: "Card.tsx",
1638
+ content: `import { cn } from "@/lib/utils";
1639
+ import type { CardProps, CardHeaderProps, CardContentProps, CardFooterProps } from "./card.types";
1640
+
1641
+ export function Card({ theme, className, children, ...props }: CardProps) {
1642
+ return (
1643
+ <div
1644
+ {...(theme ? { "data-bt-theme": theme } : {})}
1645
+ className={cn(
1646
+ "rounded-[var(--bt-radius-lg)] border border-[var(--bt-border)]",
1647
+ "bg-[var(--bt-bg-surface)] shadow-[var(--bt-shadow-sm)]",
1648
+ className
1649
+ )}
1650
+ {...props}
1651
+ >
1652
+ {children}
1653
+ </div>
1654
+ );
1655
+ }
1656
+
1657
+ export function CardHeader({ className, children, ...props }: CardHeaderProps) {
1658
+ return (
1659
+ <div className={cn("flex flex-col gap-1.5 p-6", className)} {...props}>
1660
+ {children}
1661
+ </div>
1662
+ );
1663
+ }
1664
+
1665
+ export function CardContent({ className, children, ...props }: CardContentProps) {
1666
+ return (
1667
+ <div className={cn("p-6 pt-0", className)} {...props}>
1668
+ {children}
1669
+ </div>
1670
+ );
1671
+ }
1672
+
1673
+ export function CardFooter({ className, children, ...props }: CardFooterProps) {
1674
+ return (
1675
+ <div className={cn("flex items-center p-6 pt-0", className)} {...props}>
1676
+ {children}
1677
+ </div>
1678
+ );
1679
+ }
1680
+ `
1681
+ },
1682
+ {
1683
+ name: "card.types.ts",
1684
+ content: `import type { HTMLAttributes } from "react";
1685
+ import type { BtTheme } from "../../types";
1686
+
1687
+ export interface CardProps extends HTMLAttributes<HTMLDivElement> {
1688
+ theme?: BtTheme;
1689
+ }
1690
+
1691
+ export interface CardHeaderProps extends HTMLAttributes<HTMLDivElement> {}
1692
+ export interface CardContentProps extends HTMLAttributes<HTMLDivElement> {}
1693
+ export interface CardFooterProps extends HTMLAttributes<HTMLDivElement> {}
1694
+ `
1695
+ }
1696
+ ]
1697
+ };
1698
+
1699
+ // src/registry/components/badge.ts
1700
+ var badge = {
1701
+ name: "badge",
1702
+ category: "display",
1703
+ description: "Small status indicator with semantic color variants",
1704
+ infraDeps: ["tokens", "globals", "utils"],
1705
+ componentDeps: [],
1706
+ npmDeps: [],
1707
+ files: [
1708
+ {
1709
+ name: "Badge.tsx",
1710
+ content: `import { cn } from "@/lib/utils";
1711
+ import type { BadgeProps } from "./badge.types";
1712
+
1713
+ const variantClasses: Record<string, string> = {
1714
+ default: "bg-[var(--bt-accent-muted)] text-[var(--bt-accent)] border-transparent",
1715
+ secondary: "bg-[var(--bt-bg-muted)] text-[var(--bt-text-secondary)] border-transparent",
1716
+ destructive: "bg-[var(--bt-destructive-bg)] text-[var(--bt-destructive)] border-transparent",
1717
+ outline: "bg-transparent text-[var(--bt-text-primary)] border-[var(--bt-border)]",
1718
+ };
1719
+
1720
+ const sizeClasses: Record<string, string> = {
1721
+ sm: "px-2 py-0.5 text-xs",
1722
+ md: "px-2.5 py-0.5 text-sm",
1723
+ };
1724
+
1725
+ export function Badge({
1726
+ variant = "default",
1727
+ size = "sm",
1728
+ theme,
1729
+ className,
1730
+ children,
1731
+ ...props
1732
+ }: BadgeProps) {
1733
+ return (
1734
+ <span
1735
+ {...(theme ? { "data-bt-theme": theme } : {})}
1736
+ className={cn(
1737
+ "inline-flex items-center font-medium border rounded-[var(--bt-radius-full)]",
1738
+ variantClasses[variant],
1739
+ sizeClasses[size],
1740
+ className
1741
+ )}
1742
+ {...props}
1743
+ >
1744
+ {children}
1745
+ </span>
1746
+ );
1747
+ }
1748
+ `
1749
+ },
1750
+ {
1751
+ name: "badge.types.ts",
1752
+ content: `import type { HTMLAttributes } from "react";
1753
+ import type { BtTheme } from "../../types";
1754
+
1755
+ export type BadgeVariant = "default" | "secondary" | "destructive" | "outline";
1756
+ export type BadgeSize = "sm" | "md";
1757
+
1758
+ export interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {
1759
+ variant?: BadgeVariant;
1760
+ size?: BadgeSize;
1761
+ theme?: BtTheme;
1762
+ }
1763
+ `
1764
+ }
1765
+ ]
1766
+ };
1767
+
1768
+ // src/registry/components/avatar.ts
1769
+ var avatar = {
1770
+ name: "avatar",
1771
+ category: "display",
1772
+ description: "User avatar with image, initials, and fallback support",
1773
+ infraDeps: ["tokens", "globals", "utils"],
1774
+ componentDeps: [],
1775
+ npmDeps: [],
1776
+ files: [
1777
+ {
1778
+ name: "Avatar.tsx",
1779
+ content: `"use client";
1780
+
1781
+ import { useState } from "react";
1782
+ import { cn } from "@/lib/utils";
1783
+ import type { AvatarProps, AvatarImageProps, AvatarFallbackProps } from "./avatar.types";
1784
+
1785
+ const sizeClasses: Record<string, string> = {
1786
+ sm: "h-6 w-6 text-xs",
1787
+ md: "h-10 w-10 text-sm",
1788
+ lg: "h-14 w-14 text-base",
1789
+ xl: "h-[72px] w-[72px] text-lg",
1790
+ };
1791
+
1792
+ export function Avatar({ size = "md", theme, className, children, ...props }: AvatarProps) {
1793
+ return (
1794
+ <div
1795
+ {...(theme ? { "data-bt-theme": theme } : {})}
1796
+ className={cn(
1797
+ "relative inline-flex items-center justify-center",
1798
+ "rounded-full overflow-hidden",
1799
+ "bg-[var(--bt-bg-muted)] text-[var(--bt-text-secondary)]",
1800
+ sizeClasses[size],
1801
+ className
1802
+ )}
1803
+ {...props}
1804
+ >
1805
+ {children}
1806
+ </div>
1807
+ );
1808
+ }
1809
+
1810
+ export function AvatarImage({ src, alt = "", className, onError, ...props }: AvatarImageProps) {
1811
+ const [errored, setErrored] = useState(false);
1812
+
1813
+ if (errored) return null;
1814
+
1815
+ return (
1816
+ <img
1817
+ src={src}
1818
+ alt={alt}
1819
+ className={cn("h-full w-full object-cover", className)}
1820
+ onError={(e) => {
1821
+ setErrored(true);
1822
+ onError?.(e);
1823
+ }}
1824
+ {...props}
1825
+ />
1826
+ );
1827
+ }
1828
+
1829
+ export function AvatarFallback({ className, children, ...props }: AvatarFallbackProps) {
1830
+ return (
1831
+ <div
1832
+ aria-hidden="true"
1833
+ className={cn(
1834
+ "absolute inset-0 flex items-center justify-center",
1835
+ "font-medium text-[var(--bt-text-secondary)]",
1836
+ className
1837
+ )}
1838
+ {...props}
1839
+ >
1840
+ {children}
1841
+ </div>
1842
+ );
1843
+ }
1844
+ `
1845
+ },
1846
+ {
1847
+ name: "avatar.types.ts",
1848
+ content: `import type { HTMLAttributes, ImgHTMLAttributes } from "react";
1849
+ import type { BtTheme } from "../../types";
1850
+
1851
+ export type AvatarSize = "sm" | "md" | "lg" | "xl";
1852
+
1853
+ export interface AvatarProps extends HTMLAttributes<HTMLDivElement> {
1854
+ size?: AvatarSize;
1855
+ theme?: BtTheme;
1856
+ }
1857
+
1858
+ export interface AvatarImageProps extends ImgHTMLAttributes<HTMLImageElement> {}
1859
+
1860
+ export interface AvatarFallbackProps extends HTMLAttributes<HTMLDivElement> {}
1861
+ `
1862
+ }
1863
+ ]
1864
+ };
1865
+
1866
+ // src/registry/components/separator.ts
1867
+ var separator = {
1868
+ name: "separator",
1869
+ category: "display",
1870
+ description: "Horizontal or vertical visual divider",
1871
+ infraDeps: ["tokens", "globals", "utils"],
1872
+ componentDeps: [],
1873
+ npmDeps: [],
1874
+ files: [
1875
+ {
1876
+ name: "Separator.tsx",
1877
+ content: `import { cn } from "@/lib/utils";
1878
+ import type { SeparatorProps } from "./separator.types";
1879
+
1880
+ export function Separator({
1881
+ orientation = "horizontal",
1882
+ theme,
1883
+ className,
1884
+ ...props
1885
+ }: SeparatorProps) {
1886
+ if (orientation === "vertical") {
1887
+ return (
1888
+ <div
1889
+ role="separator"
1890
+ aria-orientation="vertical"
1891
+ {...(theme ? { "data-bt-theme": theme } : {})}
1892
+ className={cn("inline-block w-px self-stretch bg-[var(--bt-border)]", className)}
1893
+ {...props}
1894
+ />
1895
+ );
1896
+ }
1897
+
1898
+ return (
1899
+ <hr
1900
+ role="separator"
1901
+ {...(theme ? { "data-bt-theme": theme } : {})}
1902
+ className={cn("border-0 border-t border-[var(--bt-border)] w-full", className)}
1903
+ {...props}
1904
+ />
1905
+ );
1906
+ }
1907
+ `
1908
+ },
1909
+ {
1910
+ name: "separator.types.ts",
1911
+ content: `import type { HTMLAttributes } from "react";
1912
+ import type { BtTheme } from "../../types";
1913
+
1914
+ export type SeparatorOrientation = "horizontal" | "vertical";
1915
+
1916
+ export interface SeparatorProps extends HTMLAttributes<HTMLElement> {
1917
+ orientation?: SeparatorOrientation;
1918
+ theme?: BtTheme;
1919
+ }
1920
+ `
1921
+ }
1922
+ ]
1923
+ };
1924
+
1925
+ // src/registry/components/skeleton.ts
1926
+ var skeleton = {
1927
+ name: "skeleton",
1928
+ category: "display",
1929
+ description: "Loading placeholder with shimmer animation",
1930
+ infraDeps: ["tokens", "globals", "utils"],
1931
+ componentDeps: [],
1932
+ npmDeps: [],
1933
+ files: [
1934
+ {
1935
+ name: "Skeleton.tsx",
1936
+ content: `import { cn } from "@/lib/utils";
1937
+ import type { SkeletonProps } from "./skeleton.types";
1938
+
1939
+ const shapeClasses: Record<string, string> = {
1940
+ line: "h-4 w-full rounded-full",
1941
+ rect: "rounded-[var(--bt-radius-md)]",
1942
+ circle: "rounded-full",
1943
+ };
1944
+
1945
+ export function Skeleton({ shape = "line", width, height, className }: SkeletonProps) {
1946
+ return (
1947
+ <div
1948
+ style={{
1949
+ width: width ? (typeof width === "number" ? \`\${width}px\` : width) : undefined,
1950
+ height: height ? (typeof height === "number" ? \`\${height}px\` : height) : undefined,
1951
+ background: "linear-gradient(90deg, var(--bt-skeleton-base) 25%, var(--bt-skeleton-highlight) 50%, var(--bt-skeleton-base) 75%)",
1952
+ backgroundSize: "200% 100%",
1953
+ animation: "bt-shimmer 1.5s ease-in-out infinite",
1954
+ }}
1955
+ className={cn(shapeClasses[shape], className)}
1956
+ />
1957
+ );
1958
+ }
1959
+ `
1960
+ },
1961
+ {
1962
+ name: "skeleton.types.ts",
1963
+ content: `export type SkeletonShape = "line" | "rect" | "circle";
1964
+
1965
+ export interface SkeletonProps {
1966
+ shape?: SkeletonShape;
1967
+ width?: string | number;
1968
+ height?: string | number;
1969
+ className?: string;
1970
+ }
1971
+ `
1972
+ }
1973
+ ]
1974
+ };
1975
+
1976
+ // src/registry/components/stat.ts
1977
+ var stat = {
1978
+ name: "stat",
1979
+ category: "display",
1980
+ description: "Metric display with label, value, and optional delta",
1981
+ infraDeps: ["tokens", "globals", "utils"],
1982
+ componentDeps: [],
1983
+ npmDeps: [],
1984
+ files: [
1985
+ {
1986
+ name: "Stat.tsx",
1987
+ content: `import { cn } from "@/lib/utils";
1988
+ import type { StatProps } from "./stat.types";
1989
+
1990
+ const trendColorMap: Record<string, string> = {
1991
+ up: "var(--bt-success)",
1992
+ down: "var(--bt-destructive)",
1993
+ neutral: "var(--bt-text-muted)",
1994
+ };
1995
+
1996
+ export function Stat({
1997
+ value,
1998
+ label,
1999
+ delta,
2000
+ trend = "neutral",
2001
+ description,
2002
+ theme,
2003
+ className,
2004
+ }: StatProps) {
2005
+ return (
2006
+ <div
2007
+ {...(theme ? { "data-bt-theme": theme } : {})}
2008
+ className={cn("flex flex-col gap-1", className)}
2009
+ >
2010
+ <p className="text-sm font-medium text-[var(--bt-text-muted)]">{label}</p>
2011
+ <p className="text-3xl font-bold text-[var(--bt-text-primary)]">{value}</p>
2012
+ {delta && (
2013
+ <p className="text-sm font-medium" style={{ color: trendColorMap[trend] }}>
2014
+ {delta}
2015
+ </p>
2016
+ )}
2017
+ {description && (
2018
+ <p className="text-xs text-[var(--bt-text-muted)]">{description}</p>
2019
+ )}
2020
+ </div>
2021
+ );
2022
+ }
2023
+ `
2024
+ },
2025
+ {
2026
+ name: "stat.types.ts",
2027
+ content: `import type { BtTheme } from "../../types";
2028
+
2029
+ export type StatTrend = "up" | "down" | "neutral";
2030
+
2031
+ export interface StatProps {
2032
+ value: string | number;
2033
+ label: string;
2034
+ delta?: string;
2035
+ trend?: StatTrend;
2036
+ description?: string;
2037
+ theme?: BtTheme;
2038
+ className?: string;
2039
+ }
2040
+ `
2041
+ }
2042
+ ]
2043
+ };
2044
+
2045
+ // src/registry/components/empty.ts
2046
+ var empty = {
2047
+ name: "empty",
2048
+ category: "display",
2049
+ description: "Empty state display with icon, heading, and action",
2050
+ infraDeps: ["tokens", "globals", "utils"],
2051
+ componentDeps: [],
2052
+ npmDeps: [],
2053
+ files: [
2054
+ {
2055
+ name: "Empty.tsx",
2056
+ content: `import { cn } from "@/lib/utils";
2057
+ import type { EmptyProps } from "./empty.types";
2058
+
2059
+ const DefaultIcon = () => (
2060
+ <svg
2061
+ width="48"
2062
+ height="48"
2063
+ viewBox="0 0 24 24"
2064
+ fill="none"
2065
+ stroke="currentColor"
2066
+ strokeWidth="1.5"
2067
+ strokeLinecap="round"
2068
+ strokeLinejoin="round"
2069
+ aria-hidden="true"
2070
+ >
2071
+ <path d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7" />
2072
+ <path d="M2 13h4l2 3h8l2-3h4" />
2073
+ </svg>
2074
+ );
2075
+
2076
+ export function Empty({ title, description, icon, action, className }: EmptyProps) {
2077
+ return (
2078
+ <div
2079
+ className={cn(
2080
+ "flex flex-col items-center justify-center gap-3 max-w-xs mx-auto text-center py-12",
2081
+ className
2082
+ )}
2083
+ >
2084
+ <div className="text-[var(--bt-text-muted)]">
2085
+ {icon ?? <DefaultIcon />}
2086
+ </div>
2087
+ <h3 className="text-base font-semibold text-[var(--bt-text-primary)]">{title}</h3>
2088
+ {description && (
2089
+ <p className="text-sm text-[var(--bt-text-muted)]">{description}</p>
2090
+ )}
2091
+ {action && <div className="mt-2">{action}</div>}
2092
+ </div>
2093
+ );
2094
+ }
2095
+ `
2096
+ },
2097
+ {
2098
+ name: "empty.types.ts",
2099
+ content: `import type { ReactNode } from "react";
2100
+
2101
+ export interface EmptyProps {
2102
+ title: string;
2103
+ description?: string;
2104
+ icon?: ReactNode;
2105
+ action?: ReactNode;
2106
+ className?: string;
2107
+ }
2108
+ `
2109
+ }
2110
+ ]
2111
+ };
2112
+
2113
+ // src/registry/components/alert.ts
2114
+ var alert = {
2115
+ name: "alert",
2116
+ category: "display",
2117
+ description: "Contextual feedback message with semantic variants",
2118
+ infraDeps: ["tokens", "globals", "utils"],
2119
+ componentDeps: [],
2120
+ npmDeps: [],
2121
+ files: [
2122
+ {
2123
+ name: "Alert.tsx",
2124
+ content: `import { cn } from "@/lib/utils";
2125
+ import type { AlertProps, AlertTitleProps, AlertDescriptionProps, AlertVariant } from "./alert.types";
2126
+
2127
+ const variantStyles: Record<AlertVariant, { bg: string; borderColor: string; accent: string }> = {
2128
+ default: {
2129
+ bg: "var(--bt-bg-surface)",
2130
+ borderColor: "var(--bt-border)",
2131
+ accent: "var(--bt-text-secondary)",
2132
+ },
2133
+ info: {
2134
+ bg: "color-mix(in srgb, var(--bt-accent) 10%, transparent)",
2135
+ borderColor: "color-mix(in srgb, var(--bt-accent) 30%, transparent)",
2136
+ accent: "var(--bt-accent)",
2137
+ },
2138
+ success: {
2139
+ bg: "color-mix(in srgb, var(--bt-success) 12%, transparent)",
2140
+ borderColor: "color-mix(in srgb, var(--bt-success) 35%, transparent)",
2141
+ accent: "var(--bt-success)",
2142
+ },
2143
+ warning: {
2144
+ bg: "color-mix(in srgb, var(--bt-warning) 12%, transparent)",
2145
+ borderColor: "color-mix(in srgb, var(--bt-warning) 35%, transparent)",
2146
+ accent: "var(--bt-warning)",
2147
+ },
2148
+ error: {
2149
+ bg: "color-mix(in srgb, var(--bt-destructive) 12%, transparent)",
2150
+ borderColor: "color-mix(in srgb, var(--bt-destructive) 35%, transparent)",
2151
+ accent: "var(--bt-destructive)",
2152
+ },
2153
+ };
2154
+
2155
+ function DefaultIcon({ variant }: { variant: AlertVariant }) {
2156
+ if (variant === "success") {
2157
+ return (
2158
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
2159
+ <circle cx="12" cy="12" r="10" />
2160
+ <polyline points="9 12 11 14 15 10" />
2161
+ </svg>
2162
+ );
2163
+ }
2164
+ if (variant === "warning") {
2165
+ return (
2166
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
2167
+ <path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
2168
+ <line x1="12" y1="9" x2="12" y2="13" />
2169
+ <line x1="12" y1="17" x2="12.01" y2="17" />
2170
+ </svg>
2171
+ );
2172
+ }
2173
+ if (variant === "error") {
2174
+ return (
2175
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
2176
+ <circle cx="12" cy="12" r="10" />
2177
+ <line x1="15" y1="9" x2="9" y2="15" />
2178
+ <line x1="9" y1="9" x2="15" y2="15" />
2179
+ </svg>
2180
+ );
2181
+ }
2182
+ // default and info
2183
+ return (
2184
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
2185
+ <circle cx="12" cy="12" r="10" />
2186
+ <line x1="12" y1="8" x2="12" y2="12" />
2187
+ <line x1="12" y1="16" x2="12.01" y2="16" />
2188
+ </svg>
2189
+ );
2190
+ }
2191
+
2192
+ export function Alert({
2193
+ variant = "default",
2194
+ icon,
2195
+ theme,
2196
+ className,
2197
+ children,
2198
+ style,
2199
+ ...props
2200
+ }: AlertProps) {
2201
+ const vs = variantStyles[variant];
2202
+ return (
2203
+ <div
2204
+ role="alert"
2205
+ {...(theme ? { "data-bt-theme": theme } : {})}
2206
+ style={{
2207
+ background: vs.bg,
2208
+ borderLeftColor: vs.borderColor,
2209
+ ...style,
2210
+ }}
2211
+ className={cn(
2212
+ "flex gap-3 p-4 rounded-[var(--bt-radius-md)] border border-transparent border-l-4",
2213
+ className
2214
+ )}
2215
+ {...props}
2216
+ >
2217
+ <span style={{ color: vs.accent }} className="mt-0.5 shrink-0">
2218
+ {icon ?? <DefaultIcon variant={variant} />}
2219
+ </span>
2220
+ <div className="flex-1 space-y-1">
2221
+ {children}
2222
+ </div>
2223
+ </div>
2224
+ );
2225
+ }
2226
+
2227
+ export function AlertTitle({ className, children, ...props }: AlertTitleProps) {
2228
+ return (
2229
+ <p className={cn("font-semibold text-sm text-[var(--bt-text-primary)] leading-tight", className)} {...props}>
2230
+ {children}
2231
+ </p>
2232
+ );
2233
+ }
2234
+
2235
+ export function AlertDescription({ className, children, ...props }: AlertDescriptionProps) {
2236
+ return (
2237
+ <p className={cn("text-sm text-[var(--bt-text-secondary)]", className)} {...props}>
2238
+ {children}
2239
+ </p>
2240
+ );
2241
+ }
2242
+ `
2243
+ },
2244
+ {
2245
+ name: "alert.types.ts",
2246
+ content: `import type { HTMLAttributes, ReactNode } from "react";
2247
+ import type { BtTheme } from "../../types";
2248
+
2249
+ export type AlertVariant = "default" | "info" | "success" | "warning" | "error";
2250
+
2251
+ export interface AlertProps extends HTMLAttributes<HTMLDivElement> {
2252
+ variant?: AlertVariant;
2253
+ icon?: ReactNode;
2254
+ theme?: BtTheme;
2255
+ }
2256
+
2257
+ export type AlertTitleProps = HTMLAttributes<HTMLParagraphElement>;
2258
+ export type AlertDescriptionProps = HTMLAttributes<HTMLParagraphElement>;
2259
+ `
2260
+ }
2261
+ ]
2262
+ };
2263
+
2264
+ // src/registry/components/progress.ts
2265
+ var progress = {
2266
+ name: "progress",
2267
+ category: "display",
2268
+ description: "Progress bar with determinate and indeterminate modes",
2269
+ infraDeps: ["tokens", "globals", "utils"],
2270
+ componentDeps: [],
2271
+ npmDeps: [],
2272
+ files: [
2273
+ {
2274
+ name: "Progress.tsx",
2275
+ content: `import { cn } from "@/lib/utils";
2276
+ import type { ProgressProps } from "./progress.types";
2277
+
2278
+ const sizeClasses = {
2279
+ sm: "h-1",
2280
+ md: "h-2",
2281
+ lg: "h-4",
2282
+ };
2283
+
2284
+ const variantColors: Record<"default" | "success" | "warning" | "error", string> = {
2285
+ default: "var(--bt-accent)",
2286
+ success: "var(--bt-success)",
2287
+ warning: "var(--bt-warning)",
2288
+ error: "var(--bt-destructive)",
2289
+ };
2290
+
2291
+ export function Progress({
2292
+ value,
2293
+ max = 100,
2294
+ size = "md",
2295
+ variant = "default",
2296
+ theme,
2297
+ className,
2298
+ style,
2299
+ ...props
2300
+ }: ProgressProps) {
2301
+ const isIndeterminate = value === undefined;
2302
+ const pct = isIndeterminate ? undefined : Math.min(100, Math.max(0, (value / max) * 100));
2303
+
2304
+ return (
2305
+ <div
2306
+ role="progressbar"
2307
+ aria-valuenow={isIndeterminate ? undefined : value}
2308
+ aria-valuemin={0}
2309
+ aria-valuemax={max}
2310
+ {...(theme ? { "data-bt-theme": theme } : {})}
2311
+ style={style}
2312
+ className={cn(
2313
+ "w-full rounded-full bg-[var(--bt-border)] overflow-hidden",
2314
+ sizeClasses[size],
2315
+ className
2316
+ )}
2317
+ {...props}
2318
+ >
2319
+ <div
2320
+ style={{
2321
+ width: isIndeterminate ? "50%" : \`\${pct}%\`,
2322
+ background: variantColors[variant],
2323
+ height: "100%",
2324
+ borderRadius: "inherit",
2325
+ transition: isIndeterminate ? undefined : "width 0.3s ease",
2326
+ animation: isIndeterminate
2327
+ ? "bt-progress-indeterminate 1.5s ease-in-out infinite"
2328
+ : undefined,
2329
+ }}
2330
+ />
2331
+ </div>
2332
+ );
2333
+ }
2334
+ `
2335
+ },
2336
+ {
2337
+ name: "progress.types.ts",
2338
+ content: `import type { HTMLAttributes } from "react";
2339
+ import type { BtTheme } from "../../types";
2340
+
2341
+ export interface ProgressProps extends HTMLAttributes<HTMLDivElement> {
2342
+ value?: number;
2343
+ max?: number;
2344
+ size?: "sm" | "md" | "lg";
2345
+ variant?: "default" | "success" | "warning" | "error";
2346
+ theme?: BtTheme;
2347
+ }
2348
+ `
2349
+ }
2350
+ ]
2351
+ };
2352
+
2353
+ // src/registry/components/dialog.ts
2354
+ var dialog = {
2355
+ name: "dialog",
2356
+ category: "overlay",
2357
+ description: "Modal dialog built on the native <dialog> element",
2358
+ infraDeps: ["tokens", "globals", "provider", "utils"],
2359
+ componentDeps: [],
2360
+ npmDeps: [],
2361
+ files: [
2362
+ {
2363
+ name: "Dialog.tsx",
2364
+ content: `"use client";
2365
+
2366
+ import {
2367
+ createContext,
2368
+ useContext,
2369
+ useRef,
2370
+ useEffect,
2371
+ useCallback,
2372
+ useId,
2373
+ type ReactNode,
2374
+ } from "react";
2375
+ import { cn } from "@/lib/utils";
2376
+ import { useBakeThereTheme } from "../../provider";
2377
+ import type {
2378
+ DialogProps,
2379
+ DialogTriggerProps,
2380
+ DialogContentProps,
2381
+ DialogHeaderProps,
2382
+ DialogTitleProps,
2383
+ DialogDescriptionProps,
2384
+ DialogFooterProps,
2385
+ DialogCloseProps,
2386
+ } from "./dialog.types";
2387
+
2388
+ interface DialogContextValue {
2389
+ open: boolean;
2390
+ onOpenChange: (open: boolean) => void;
2391
+ titleId: string;
2392
+ descriptionId: string;
2393
+ }
2394
+
2395
+ const DialogContext = createContext<DialogContextValue | null>(null);
2396
+
2397
+ function useDialogContext() {
2398
+ const ctx = useContext(DialogContext);
2399
+ if (!ctx) throw new Error("Dialog sub-components must be used inside <Dialog>");
2400
+ return ctx;
2401
+ }
2402
+
2403
+ export function Dialog({ open = false, onOpenChange, children, theme }: DialogProps) {
2404
+ const { theme: contextTheme } = useBakeThereTheme();
2405
+ const activeTheme = theme ?? contextTheme;
2406
+ const titleId = useId();
2407
+ const descriptionId = useId();
2408
+
2409
+ const handleOpenChange = useCallback(
2410
+ (val: boolean) => onOpenChange?.(val),
2411
+ [onOpenChange]
2412
+ );
2413
+
2414
+ return (
2415
+ <DialogContext.Provider
2416
+ value={{ open, onOpenChange: handleOpenChange, titleId, descriptionId }}
2417
+ >
2418
+ <div data-bt-theme={activeTheme}>{children}</div>
2419
+ </DialogContext.Provider>
2420
+ );
2421
+ }
2422
+
2423
+ export function DialogTrigger({ children, onClick, className, ...props }: DialogTriggerProps) {
2424
+ const { onOpenChange } = useDialogContext();
2425
+ return (
2426
+ <button
2427
+ type="button"
2428
+ onClick={(e) => {
2429
+ onOpenChange(true);
2430
+ onClick?.(e);
2431
+ }}
2432
+ className={cn(
2433
+ "inline-flex items-center gap-1.5 px-4 py-2 text-sm font-medium",
2434
+ "rounded-[var(--bt-radius-md)] border border-[var(--bt-border)]",
2435
+ "text-[var(--bt-text-primary)] bg-transparent hover:bg-[var(--bt-hover-bg)]",
2436
+ "transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--bt-ring)]",
2437
+ "disabled:opacity-[var(--bt-disabled-opacity)] disabled:pointer-events-none",
2438
+ className
2439
+ )}
2440
+ {...props}
2441
+ >
2442
+ {children}
2443
+ </button>
2444
+ );
2445
+ }
2446
+
2447
+ export function DialogContent({ children, onClose, className, ...props }: DialogContentProps) {
2448
+ const { open, onOpenChange, titleId, descriptionId } = useDialogContext();
2449
+ const dialogRef = useRef<HTMLDialogElement>(null);
2450
+
2451
+ useEffect(() => {
2452
+ const dialog = dialogRef.current;
2453
+ if (!dialog) return;
2454
+ if (open && !dialog.open) {
2455
+ dialog.showModal();
2456
+ } else if (!open && dialog.open) {
2457
+ dialog.close();
2458
+ }
2459
+ }, [open]);
2460
+
2461
+ useEffect(() => {
2462
+ const dialog = dialogRef.current;
2463
+ if (!dialog) return;
2464
+ const handleClose = () => onOpenChange(false);
2465
+ dialog.addEventListener("close", handleClose);
2466
+ return () => dialog.removeEventListener("close", handleClose);
2467
+ }, [onOpenChange]);
2468
+
2469
+ return (
2470
+ <dialog
2471
+ ref={dialogRef}
2472
+ aria-labelledby={titleId}
2473
+ aria-describedby={descriptionId}
2474
+ aria-modal="true"
2475
+ className={cn(
2476
+ "rounded-[var(--bt-radius-lg)] border border-[var(--bt-border)]",
2477
+ "bg-[var(--bt-bg-elevated)] text-[var(--bt-text-primary)]",
2478
+ "shadow-[var(--bt-shadow-lg)] p-0 max-w-lg w-full",
2479
+ "open:flex open:flex-col",
2480
+ className
2481
+ )}
2482
+ onClick={(e) => {
2483
+ if (e.target === e.currentTarget) onOpenChange(false);
2484
+ }}
2485
+ {...props}
2486
+ >
2487
+ <div className="flex flex-col p-6 gap-4 animate-[bt-dialog-in_0.2s_ease-out]">{children}</div>
2488
+ </dialog>
2489
+ );
2490
+ }
2491
+
2492
+ export function DialogHeader({ className, children, ...props }: DialogHeaderProps) {
2493
+ return (
2494
+ <div className={cn("flex flex-col gap-1.5", className)} {...props}>
2495
+ {children}
2496
+ </div>
2497
+ );
2498
+ }
2499
+
2500
+ export function DialogTitle({ className, children, ...props }: DialogTitleProps) {
2501
+ const { titleId } = useDialogContext();
2502
+ return (
2503
+ <h2
2504
+ id={titleId}
2505
+ className={cn("text-lg font-semibold text-[var(--bt-text-primary)]", className)}
2506
+ {...props}
2507
+ >
2508
+ {children}
2509
+ </h2>
2510
+ );
2511
+ }
2512
+
2513
+ export function DialogDescription({ className, children, ...props }: DialogDescriptionProps) {
2514
+ const { descriptionId } = useDialogContext();
2515
+ return (
2516
+ <p
2517
+ id={descriptionId}
2518
+ className={cn("text-sm text-[var(--bt-text-muted)]", className)}
2519
+ {...props}
2520
+ >
2521
+ {children}
2522
+ </p>
2523
+ );
2524
+ }
2525
+
2526
+ export function DialogFooter({ className, children, ...props }: DialogFooterProps) {
2527
+ return (
2528
+ <div
2529
+ className={cn("flex items-center justify-end gap-3 pt-2", className)}
2530
+ {...props}
2531
+ >
2532
+ {children}
2533
+ </div>
2534
+ );
2535
+ }
2536
+
2537
+ export function DialogClose({ children, onClick, className, ...props }: DialogCloseProps) {
2538
+ const { onOpenChange } = useDialogContext();
2539
+ return (
2540
+ <button
2541
+ type="button"
2542
+ onClick={(e) => {
2543
+ onOpenChange(false);
2544
+ onClick?.(e);
2545
+ }}
2546
+ className={cn(
2547
+ "inline-flex items-center justify-center rounded-[var(--bt-radius-md)]",
2548
+ "text-sm font-medium transition-colors",
2549
+ "text-[var(--bt-text-muted)] hover:text-[var(--bt-text-primary)]",
2550
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--bt-ring)]",
2551
+ className
2552
+ )}
2553
+ {...props}
2554
+ >
2555
+ {children}
2556
+ </button>
2557
+ );
2558
+ }
2559
+ `
2560
+ },
2561
+ {
2562
+ name: "dialog.types.ts",
2563
+ content: `import type { HTMLAttributes, DialogHTMLAttributes, ReactNode, ButtonHTMLAttributes } from "react";
2564
+ import type { BtTheme } from "../../types";
2565
+
2566
+ export interface DialogProps {
2567
+ open?: boolean;
2568
+ onOpenChange?: (open: boolean) => void;
2569
+ children: ReactNode;
2570
+ theme?: BtTheme;
2571
+ }
2572
+
2573
+ export interface DialogTriggerProps extends ButtonHTMLAttributes<HTMLButtonElement> {
2574
+ asChild?: boolean;
2575
+ }
2576
+
2577
+ export interface DialogContentProps extends DialogHTMLAttributes<HTMLDialogElement> {
2578
+ onClose?: () => void;
2579
+ }
2580
+
2581
+ export interface DialogHeaderProps extends HTMLAttributes<HTMLDivElement> {}
2582
+ export interface DialogTitleProps extends HTMLAttributes<HTMLHeadingElement> {}
2583
+ export interface DialogDescriptionProps extends HTMLAttributes<HTMLParagraphElement> {}
2584
+ export interface DialogFooterProps extends HTMLAttributes<HTMLDivElement> {}
2585
+ export interface DialogCloseProps extends ButtonHTMLAttributes<HTMLButtonElement> {}
2586
+ `
2587
+ }
2588
+ ]
2589
+ };
2590
+
2591
+ // src/registry/components/tooltip.ts
2592
+ var tooltip = {
2593
+ name: "tooltip",
2594
+ category: "overlay",
2595
+ description: "CSS-only tooltip with directional placement",
2596
+ infraDeps: ["tokens", "globals", "utils"],
2597
+ componentDeps: [],
2598
+ npmDeps: [],
2599
+ files: [
2600
+ {
2601
+ name: "Tooltip.tsx",
2602
+ content: `import { cn } from "@/lib/utils";
2603
+ import type { TooltipProps } from "./tooltip.types";
2604
+
2605
+ export function Tooltip({
2606
+ content,
2607
+ side = "top",
2608
+ delay = 0,
2609
+ theme,
2610
+ className,
2611
+ children,
2612
+ }: TooltipProps) {
2613
+ return (
2614
+ <div
2615
+ className={cn("bt-tooltip-wrapper relative inline-block", className)}
2616
+ {...(theme ? { "data-bt-theme": theme } : {})}
2617
+ >
2618
+ {children}
2619
+ <div
2620
+ data-bt-tooltip=""
2621
+ data-side={side}
2622
+ role="tooltip"
2623
+ style={delay ? { transitionDelay: \`\${delay}ms\` } : undefined}
2624
+ >
2625
+ {content}
2626
+ </div>
2627
+ </div>
2628
+ );
2629
+ }
2630
+ `
2631
+ },
2632
+ {
2633
+ name: "tooltip.types.ts",
2634
+ content: `import type { HTMLAttributes, ReactNode } from "react";
2635
+ import type { BtTheme } from "../../types";
2636
+
2637
+ export type TooltipSide = "top" | "bottom" | "left" | "right";
2638
+
2639
+ export interface TooltipProps {
2640
+ content: ReactNode;
2641
+ side?: TooltipSide;
2642
+ delay?: number;
2643
+ theme?: BtTheme;
2644
+ className?: string;
2645
+ children: ReactNode;
2646
+ }
2647
+ `
2648
+ }
2649
+ ]
2650
+ };
2651
+
2652
+ // src/registry/components/toast.ts
2653
+ var toast = {
2654
+ name: "toast",
2655
+ category: "overlay",
2656
+ description: "Non-blocking notification with toast queue and provider",
2657
+ infraDeps: ["tokens", "globals", "provider", "utils"],
2658
+ componentDeps: [],
2659
+ npmDeps: [],
2660
+ files: [
2661
+ {
2662
+ name: "Toast.tsx",
2663
+ content: `"use client";
2664
+
2665
+ import { useEffect } from "react";
2666
+ import { cn } from "@/lib/utils";
2667
+ import type { ToastProps } from "./toast.types";
2668
+
2669
+ const variantClasses: Record<string, string> = {
2670
+ default: "border-[var(--bt-border)] bg-[var(--bt-bg-elevated)]",
2671
+ success:
2672
+ "border-[var(--bt-success)] bg-[var(--bt-bg-elevated)] text-[var(--bt-success)]",
2673
+ destructive:
2674
+ "border-[var(--bt-destructive)] bg-[var(--bt-destructive-bg)] text-[var(--bt-destructive)]",
2675
+ warning:
2676
+ "border-[var(--bt-warning)] bg-[var(--bt-bg-elevated)] text-[var(--bt-warning)]",
2677
+ };
2678
+
2679
+ export function Toast({
2680
+ id,
2681
+ title,
2682
+ description,
2683
+ variant = "default",
2684
+ duration = 4000,
2685
+ onDismiss,
2686
+ theme,
2687
+ }: ToastProps) {
2688
+ useEffect(() => {
2689
+ if (duration <= 0) return;
2690
+ const timer = setTimeout(() => onDismiss(id), duration);
2691
+ return () => clearTimeout(timer);
2692
+ }, [id, duration, onDismiss]);
2693
+
2694
+ return (
2695
+ <div
2696
+ role="alert"
2697
+ aria-live="assertive"
2698
+ aria-atomic="true"
2699
+ {...(theme ? { "data-bt-theme": theme } : {})}
2700
+ onClick={() => onDismiss(id)}
2701
+ className={cn(
2702
+ "relative flex flex-col gap-1 rounded-[var(--bt-radius-md)] border p-4",
2703
+ "text-[var(--bt-text-primary)] shadow-[var(--bt-shadow-lg)]",
2704
+ "cursor-pointer select-none",
2705
+ "animate-[bt-toast-in_0.3s_ease-out]",
2706
+ variantClasses[variant]
2707
+ )}
2708
+ >
2709
+ {title && <p className="text-sm font-semibold">{title}</p>}
2710
+ {description && <p className="text-sm opacity-90">{description}</p>}
2711
+ </div>
2712
+ );
2713
+ }
2714
+ `
2715
+ },
2716
+ {
2717
+ name: "ToastProvider.tsx",
2718
+ content: `"use client";
2719
+
2720
+ import { createPortal } from "react-dom";
2721
+ import { useEffect, useState } from "react";
2722
+ import { useToast } from "./useToast";
2723
+ import { Toast } from "./Toast";
2724
+ import type { BtTheme } from "../../types";
2725
+
2726
+ interface ToastProviderProps {
2727
+ theme?: BtTheme;
2728
+ }
2729
+
2730
+ export function ToastProvider({ theme }: ToastProviderProps) {
2731
+ const { toasts, dismiss } = useToast();
2732
+ const [mounted, setMounted] = useState(false);
2733
+
2734
+ useEffect(() => {
2735
+ setMounted(true);
2736
+ }, []);
2737
+
2738
+ if (!mounted) return null;
2739
+
2740
+ return createPortal(
2741
+ <div
2742
+ aria-label="Notifications"
2743
+ className="fixed bottom-4 right-4 z-[100] flex flex-col gap-2 w-80 max-w-[calc(100vw-2rem)]"
2744
+ >
2745
+ {toasts.map((toast) => (
2746
+ <Toast key={toast.id} {...toast} onDismiss={dismiss} theme={theme} />
2747
+ ))}
2748
+ </div>,
2749
+ document.body
2750
+ );
2751
+ }
2752
+ `
2753
+ },
2754
+ {
2755
+ name: "useToast.ts",
2756
+ content: `"use client";
2757
+
2758
+ import { useSyncExternalStore, useCallback } from "react";
2759
+ import type { ToastItem, ToastOptions } from "./toast.types";
2760
+
2761
+ let toasts: ToastItem[] = [];
2762
+ const listeners = new Set<() => void>();
2763
+
2764
+ function notify() {
2765
+ listeners.forEach((l) => l());
2766
+ }
2767
+
2768
+ const store = {
2769
+ subscribe(listener: () => void) {
2770
+ listeners.add(listener);
2771
+ return () => listeners.delete(listener);
2772
+ },
2773
+ getSnapshot(): ToastItem[] {
2774
+ return toasts;
2775
+ },
2776
+ };
2777
+
2778
+ function addToast(options: ToastOptions): string {
2779
+ const id = Math.random().toString(36).slice(2);
2780
+ toasts = [...toasts, { id, ...options }];
2781
+ notify();
2782
+ return id;
2783
+ }
2784
+
2785
+ function removeToast(id: string): void {
2786
+ toasts = toasts.filter((t) => t.id !== id);
2787
+ notify();
2788
+ }
2789
+
2790
+ export function useToast() {
2791
+ const items = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
2792
+
2793
+ const toast = useCallback((options: ToastOptions) => {
2794
+ return addToast(options);
2795
+ }, []);
2796
+
2797
+ const dismiss = useCallback((id: string) => {
2798
+ removeToast(id);
2799
+ }, []);
2800
+
2801
+ return { toasts: items, toast, dismiss };
2802
+ }
2803
+ `
2804
+ },
2805
+ {
2806
+ name: "toast.types.ts",
2807
+ content: `import type { BtTheme } from "../../types";
2808
+
2809
+ export type ToastVariant = "default" | "success" | "destructive" | "warning";
2810
+
2811
+ export interface ToastItem {
2812
+ id: string;
2813
+ title?: string;
2814
+ description?: string;
2815
+ variant?: ToastVariant;
2816
+ duration?: number;
2817
+ }
2818
+
2819
+ export interface ToastProps extends ToastItem {
2820
+ onDismiss: (id: string) => void;
2821
+ theme?: BtTheme;
2822
+ }
2823
+
2824
+ export interface ToastOptions {
2825
+ title?: string;
2826
+ description?: string;
2827
+ variant?: ToastVariant;
2828
+ duration?: number;
2829
+ }
2830
+ `
2831
+ }
2832
+ ]
2833
+ };
2834
+
2835
+ // src/registry/components/popover.ts
2836
+ var popover = {
2837
+ name: "popover",
2838
+ category: "overlay",
2839
+ description: "Floating content panel anchored to a trigger",
2840
+ infraDeps: ["tokens", "globals", "provider", "utils"],
2841
+ componentDeps: [],
2842
+ npmDeps: [],
2843
+ files: [
2844
+ {
2845
+ name: "Popover.tsx",
2846
+ content: `"use client";
2847
+
2848
+ import {
2849
+ createContext,
2850
+ useContext,
2851
+ useState,
2852
+ useRef,
2853
+ useEffect,
2854
+ useCallback,
2855
+ useId,
2856
+ type ReactNode,
2857
+ type ReactElement,
2858
+ } from "react";
2859
+ import React from "react";
2860
+ import { cn } from "@/lib/utils";
2861
+ import { useBakeThereTheme } from "../../provider";
2862
+ import type {
2863
+ PopoverProps,
2864
+ PopoverTriggerProps,
2865
+ PopoverContentProps,
2866
+ } from "./popover.types";
2867
+
2868
+ interface PopoverContextValue {
2869
+ open: boolean;
2870
+ onOpenChange: (open: boolean) => void;
2871
+ triggerRef: React.RefObject<HTMLElement | null>;
2872
+ contentId: string;
2873
+ activeTheme: string;
2874
+ }
2875
+
2876
+ const PopoverContext = createContext<PopoverContextValue | null>(null);
2877
+
2878
+ function usePopoverContext() {
2879
+ const ctx = useContext(PopoverContext);
2880
+ if (!ctx) throw new Error("Popover sub-components must be used inside <Popover>");
2881
+ return ctx;
2882
+ }
2883
+
2884
+ export function Popover({
2885
+ open: controlledOpen,
2886
+ onOpenChange,
2887
+ defaultOpen = false,
2888
+ theme,
2889
+ children,
2890
+ }: PopoverProps) {
2891
+ const { theme: contextTheme } = useBakeThereTheme();
2892
+ const activeTheme = theme ?? contextTheme;
2893
+ const [internalOpen, setInternalOpen] = useState(defaultOpen);
2894
+ const open = controlledOpen !== undefined ? controlledOpen : internalOpen;
2895
+ const triggerRef = useRef<HTMLElement | null>(null);
2896
+ const contentId = useId();
2897
+
2898
+ const handleOpenChange = useCallback(
2899
+ (val: boolean) => {
2900
+ if (controlledOpen === undefined) setInternalOpen(val);
2901
+ onOpenChange?.(val);
2902
+ },
2903
+ [controlledOpen, onOpenChange]
2904
+ );
2905
+
2906
+ useEffect(() => {
2907
+ if (!open) return;
2908
+ function handleMouseDown(e: MouseEvent) {
2909
+ const content = document.getElementById(contentId);
2910
+ if (
2911
+ triggerRef.current &&
2912
+ !triggerRef.current.contains(e.target as Node) &&
2913
+ !content?.contains(e.target as Node)
2914
+ ) {
2915
+ handleOpenChange(false);
2916
+ }
2917
+ }
2918
+ function handleKeyDown(e: KeyboardEvent) {
2919
+ if (e.key === "Escape") handleOpenChange(false);
2920
+ }
2921
+ document.addEventListener("mousedown", handleMouseDown);
2922
+ document.addEventListener("keydown", handleKeyDown);
2923
+ return () => {
2924
+ document.removeEventListener("mousedown", handleMouseDown);
2925
+ document.removeEventListener("keydown", handleKeyDown);
2926
+ };
2927
+ }, [open, contentId, handleOpenChange]);
2928
+
2929
+ return (
2930
+ <PopoverContext.Provider value={{ open, onOpenChange: handleOpenChange, triggerRef, contentId, activeTheme }}>
2931
+ {children}
2932
+ </PopoverContext.Provider>
2933
+ );
2934
+ }
2935
+
2936
+ export function PopoverTrigger({ children }: PopoverTriggerProps) {
2937
+ const { open, onOpenChange, triggerRef, contentId } = usePopoverContext();
2938
+ const childProps = children.props as { onClick?: (e: React.MouseEvent) => void };
2939
+ return React.cloneElement(children as ReactElement<Record<string, unknown>>, {
2940
+ ref: triggerRef,
2941
+ onClick: (e: React.MouseEvent) => {
2942
+ onOpenChange(!open);
2943
+ childProps.onClick?.(e);
2944
+ },
2945
+ "aria-expanded": open,
2946
+ "aria-haspopup": "dialog",
2947
+ "aria-controls": contentId,
2948
+ });
2949
+ }
2950
+
2951
+ export function PopoverContent({
2952
+ side = "bottom",
2953
+ align = "center",
2954
+ sideOffset = 8,
2955
+ children,
2956
+ className,
2957
+ style,
2958
+ ...props
2959
+ }: PopoverContentProps) {
2960
+ const { open, triggerRef, contentId, activeTheme } = usePopoverContext();
2961
+ const contentRef = useRef<HTMLDivElement>(null);
2962
+ const [pos, setPos] = useState<{ top: number; left: number } | null>(null);
2963
+
2964
+ useEffect(() => {
2965
+ if (!open) {
2966
+ setPos(null);
2967
+ return;
2968
+ }
2969
+
2970
+ function recalculate() {
2971
+ if (!triggerRef.current || !contentRef.current) return;
2972
+ const rect = triggerRef.current.getBoundingClientRect();
2973
+ const cw = contentRef.current.offsetWidth;
2974
+ const ch = contentRef.current.offsetHeight;
2975
+
2976
+ let top = 0;
2977
+ let left = 0;
2978
+
2979
+ if (side === "bottom") {
2980
+ top = rect.bottom + sideOffset;
2981
+ } else if (side === "top") {
2982
+ top = rect.top - ch - sideOffset;
2983
+ } else if (side === "right") {
2984
+ left = rect.right + sideOffset;
2985
+ } else {
2986
+ left = rect.left - cw - sideOffset;
2987
+ }
2988
+
2989
+ if (side === "bottom" || side === "top") {
2990
+ if (align === "start") left = rect.left;
2991
+ else if (align === "center") left = rect.left + rect.width / 2 - cw / 2;
2992
+ else left = rect.right - cw;
2993
+ } else {
2994
+ if (align === "start") top = rect.top;
2995
+ else if (align === "center") top = rect.top + rect.height / 2 - ch / 2;
2996
+ else top = rect.bottom - ch;
2997
+ }
2998
+
2999
+ setPos({ top, left });
3000
+ }
3001
+
3002
+ recalculate();
3003
+ window.addEventListener("scroll", recalculate, { passive: true, capture: true });
3004
+ window.addEventListener("resize", recalculate, { passive: true });
3005
+ return () => {
3006
+ window.removeEventListener("scroll", recalculate, { capture: true });
3007
+ window.removeEventListener("resize", recalculate);
3008
+ };
3009
+ }, [open, side, align, sideOffset]);
3010
+
3011
+ if (!open) return null;
3012
+
3013
+ return (
3014
+ <div
3015
+ ref={contentRef}
3016
+ id={contentId}
3017
+ role="dialog"
3018
+ data-bt-theme={activeTheme}
3019
+ style={{
3020
+ position: "fixed",
3021
+ top: pos?.top ?? 0,
3022
+ left: pos?.left ?? 0,
3023
+ zIndex: 60,
3024
+ visibility: pos ? "visible" : "hidden",
3025
+ pointerEvents: pos ? "auto" : "none",
3026
+ animation: "bt-fade-in 0.15s ease-out",
3027
+ ...style,
3028
+ }}
3029
+ className={cn(
3030
+ "bg-[var(--bt-bg-surface)] border border-[var(--bt-border)]",
3031
+ "shadow-[var(--bt-shadow-md)] rounded-[var(--bt-radius-md)] p-3",
3032
+ className
3033
+ )}
3034
+ {...props}
3035
+ >
3036
+ {children}
3037
+ </div>
3038
+ );
3039
+ }
3040
+ `
3041
+ },
3042
+ {
3043
+ name: "popover.types.ts",
3044
+ content: `import type { HTMLAttributes, ReactElement, ReactNode } from "react";
3045
+ import type { BtTheme } from "../../types";
3046
+
3047
+ export interface PopoverProps {
3048
+ open?: boolean;
3049
+ onOpenChange?: (open: boolean) => void;
3050
+ defaultOpen?: boolean;
3051
+ theme?: BtTheme;
3052
+ children: ReactNode;
3053
+ }
3054
+
3055
+ export interface PopoverTriggerProps {
3056
+ children: ReactElement;
3057
+ }
3058
+
3059
+ export interface PopoverContentProps extends HTMLAttributes<HTMLDivElement> {
3060
+ side?: "top" | "bottom" | "left" | "right";
3061
+ align?: "start" | "center" | "end";
3062
+ sideOffset?: number;
3063
+ }
3064
+ `
3065
+ }
3066
+ ]
3067
+ };
3068
+
3069
+ // src/registry/components/dropdown-menu.ts
3070
+ var dropdownMenu = {
3071
+ name: "dropdown-menu",
3072
+ category: "navigation",
3073
+ description: "Dropdown menu with keyboard navigation",
3074
+ infraDeps: ["tokens", "globals", "provider", "utils"],
3075
+ componentDeps: [],
3076
+ npmDeps: [],
3077
+ files: [
3078
+ {
3079
+ name: "DropdownMenu.tsx",
3080
+ content: `"use client";
3081
+
3082
+ import {
3083
+ createContext,
3084
+ useContext,
3085
+ useState,
3086
+ useRef,
3087
+ useEffect,
3088
+ useCallback,
3089
+ type ReactNode,
3090
+ type KeyboardEvent,
3091
+ } from "react";
3092
+ import { cn } from "@/lib/utils";
3093
+ import { useBakeThereTheme } from "../../provider";
3094
+ import type {
3095
+ DropdownMenuProps,
3096
+ DropdownMenuTriggerProps,
3097
+ DropdownMenuContentProps,
3098
+ DropdownMenuItemProps,
3099
+ DropdownMenuSeparatorProps,
3100
+ DropdownMenuLabelProps,
3101
+ } from "./dropdown-menu.types";
3102
+
3103
+ interface DropdownContextValue {
3104
+ open: boolean;
3105
+ setOpen: (open: boolean) => void;
3106
+ }
3107
+
3108
+ const DropdownContext = createContext<DropdownContextValue | null>(null);
3109
+
3110
+ function useDropdownContext() {
3111
+ const ctx = useContext(DropdownContext);
3112
+ if (!ctx) throw new Error("DropdownMenu sub-components must be used inside <DropdownMenu>");
3113
+ return ctx;
3114
+ }
3115
+
3116
+ export function DropdownMenu({ children, theme }: DropdownMenuProps) {
3117
+ const { theme: contextTheme } = useBakeThereTheme();
3118
+ const activeTheme = theme ?? contextTheme;
3119
+ const [open, setOpen] = useState(false);
3120
+ const containerRef = useRef<HTMLDivElement>(null);
3121
+
3122
+ useEffect(() => {
3123
+ if (!open) return;
3124
+ function handleMouseDown(e: MouseEvent) {
3125
+ if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
3126
+ setOpen(false);
3127
+ }
3128
+ }
3129
+ document.addEventListener("mousedown", handleMouseDown);
3130
+ return () => document.removeEventListener("mousedown", handleMouseDown);
3131
+ }, [open]);
3132
+
3133
+ return (
3134
+ <DropdownContext.Provider value={{ open, setOpen }}>
3135
+ <div ref={containerRef} data-bt-theme={activeTheme} className="relative inline-block">
3136
+ {children}
3137
+ </div>
3138
+ </DropdownContext.Provider>
3139
+ );
3140
+ }
3141
+
3142
+ export function DropdownMenuTrigger({ children, onClick, className, ...props }: DropdownMenuTriggerProps) {
3143
+ const { open, setOpen } = useDropdownContext();
3144
+ return (
3145
+ <button
3146
+ type="button"
3147
+ aria-haspopup="true"
3148
+ aria-expanded={open}
3149
+ onClick={(e) => {
3150
+ setOpen(!open);
3151
+ onClick?.(e);
3152
+ }}
3153
+ className={cn(
3154
+ "inline-flex items-center gap-1.5 px-4 py-2 text-sm font-medium rounded-[var(--bt-radius-md)]",
3155
+ "border border-[var(--bt-border)] text-[var(--bt-text-primary)] bg-transparent",
3156
+ "hover:bg-[var(--bt-hover-bg)] transition-colors",
3157
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--bt-ring)]",
3158
+ "disabled:opacity-[var(--bt-disabled-opacity)] disabled:pointer-events-none",
3159
+ className
3160
+ )}
3161
+ {...props}
3162
+ >
3163
+ {children}
3164
+ </button>
3165
+ );
3166
+ }
3167
+
3168
+ export function DropdownMenuContent({
3169
+ children,
3170
+ align = "start",
3171
+ className,
3172
+ ...props
3173
+ }: DropdownMenuContentProps) {
3174
+ const { open } = useDropdownContext();
3175
+ const contentRef = useRef<HTMLDivElement>(null);
3176
+
3177
+ const handleKeyDown = useCallback((e: KeyboardEvent<HTMLDivElement>) => {
3178
+ const items = contentRef.current?.querySelectorAll<HTMLButtonElement>(
3179
+ '[role="menuitem"]:not(:disabled)'
3180
+ );
3181
+ if (!items || items.length === 0) return;
3182
+ const focused = document.activeElement;
3183
+ const idx = Array.from(items).indexOf(focused as HTMLButtonElement);
3184
+
3185
+ if (e.key === "ArrowDown") {
3186
+ e.preventDefault();
3187
+ items[(idx + 1) % items.length].focus();
3188
+ } else if (e.key === "ArrowUp") {
3189
+ e.preventDefault();
3190
+ items[(idx - 1 + items.length) % items.length].focus();
3191
+ } else if (e.key === "Escape") {
3192
+ e.preventDefault();
3193
+ // Find and focus the trigger
3194
+ const trigger = contentRef.current?.closest(".relative")?.querySelector<HTMLButtonElement>(
3195
+ '[aria-haspopup="true"]'
3196
+ );
3197
+ trigger?.focus();
3198
+ }
3199
+ }, []);
3200
+
3201
+ if (!open) return null;
3202
+
3203
+ return (
3204
+ <div
3205
+ ref={contentRef}
3206
+ role="menu"
3207
+ aria-orientation="vertical"
3208
+ onKeyDown={handleKeyDown}
3209
+ className={cn(
3210
+ "absolute z-50 mt-1 min-w-[160px] rounded-[var(--bt-radius-md)]",
3211
+ "border border-[var(--bt-border)] bg-[var(--bt-bg-elevated)]",
3212
+ "shadow-[var(--bt-shadow-md)] py-1",
3213
+ "animate-[bt-dropdown-in_0.15s_ease-out]",
3214
+ align === "end" ? "right-0" : align === "center" ? "left-1/2 -translate-x-1/2" : "left-0",
3215
+ className
3216
+ )}
3217
+ {...props}
3218
+ >
3219
+ {children}
3220
+ </div>
3221
+ );
3222
+ }
3223
+
3224
+ export function DropdownMenuItem({
3225
+ children,
3226
+ destructive = false,
3227
+ onClick,
3228
+ className,
3229
+ ...props
3230
+ }: DropdownMenuItemProps) {
3231
+ const { setOpen } = useDropdownContext();
3232
+ return (
3233
+ <button
3234
+ type="button"
3235
+ role="menuitem"
3236
+ onClick={(e) => {
3237
+ onClick?.(e);
3238
+ setOpen(false);
3239
+ }}
3240
+ className={cn(
3241
+ "w-full flex items-center px-3 py-2 text-sm text-left",
3242
+ "focus-visible:outline-none focus-visible:bg-[var(--bt-hover-bg)]",
3243
+ "hover:bg-[var(--bt-hover-bg)] transition-colors",
3244
+ destructive
3245
+ ? "text-[var(--bt-destructive)] hover:bg-[var(--bt-destructive-bg)]"
3246
+ : "text-[var(--bt-text-primary)]",
3247
+ className
3248
+ )}
3249
+ {...props}
3250
+ >
3251
+ {children}
3252
+ </button>
3253
+ );
3254
+ }
3255
+
3256
+ export function DropdownMenuSeparator({ className, ...props }: DropdownMenuSeparatorProps) {
3257
+ return (
3258
+ <div
3259
+ role="separator"
3260
+ className={cn("my-1 h-px bg-[var(--bt-border)]", className)}
3261
+ {...props}
3262
+ />
3263
+ );
3264
+ }
3265
+
3266
+ export function DropdownMenuLabel({ children, className, ...props }: DropdownMenuLabelProps) {
3267
+ return (
3268
+ <div
3269
+ className={cn("px-3 py-1.5 text-xs font-semibold text-[var(--bt-text-muted)]", className)}
3270
+ {...props}
3271
+ >
3272
+ {children}
3273
+ </div>
3274
+ );
3275
+ }
3276
+ `
3277
+ },
3278
+ {
3279
+ name: "dropdown-menu.types.ts",
3280
+ content: `import type { HTMLAttributes, ButtonHTMLAttributes, ReactNode } from "react";
3281
+ import type { BtTheme } from "../../types";
3282
+
3283
+ export interface DropdownMenuProps {
3284
+ children: ReactNode;
3285
+ theme?: BtTheme;
3286
+ }
3287
+
3288
+ export interface DropdownMenuTriggerProps extends ButtonHTMLAttributes<HTMLButtonElement> {}
3289
+
3290
+ export interface DropdownMenuContentProps extends HTMLAttributes<HTMLDivElement> {
3291
+ align?: "start" | "end" | "center";
3292
+ }
3293
+
3294
+ export interface DropdownMenuItemProps extends ButtonHTMLAttributes<HTMLButtonElement> {
3295
+ destructive?: boolean;
3296
+ }
3297
+
3298
+ export interface DropdownMenuSeparatorProps extends HTMLAttributes<HTMLDivElement> {}
3299
+
3300
+ export interface DropdownMenuLabelProps extends HTMLAttributes<HTMLDivElement> {}
3301
+ `
3302
+ }
3303
+ ]
3304
+ };
3305
+
3306
+ // src/registry/components/tabs.ts
3307
+ var tabs = {
3308
+ name: "tabs",
3309
+ category: "navigation",
3310
+ description: "Tab navigation with single-selection content panels",
3311
+ infraDeps: ["tokens", "globals", "provider", "utils"],
3312
+ componentDeps: [],
3313
+ npmDeps: [],
3314
+ files: [
3315
+ {
3316
+ name: "Tabs.tsx",
3317
+ content: `"use client";
3318
+
3319
+ import {
3320
+ createContext,
3321
+ useContext,
3322
+ useState,
3323
+ useCallback,
3324
+ useRef,
3325
+ type KeyboardEvent,
3326
+ } from "react";
3327
+ import { cn } from "@/lib/utils";
3328
+ import { useBakeThereTheme } from "../../provider";
3329
+ import type {
3330
+ TabsProps,
3331
+ TabsListProps,
3332
+ TabsTriggerProps,
3333
+ TabsContentProps,
3334
+ } from "./tabs.types";
3335
+
3336
+ interface TabsContextValue {
3337
+ activeValue: string;
3338
+ setActiveValue: (value: string) => void;
3339
+ variant: "line" | "pill";
3340
+ }
3341
+
3342
+ const TabsContext = createContext<TabsContextValue | null>(null);
3343
+
3344
+ function useTabsContext() {
3345
+ const ctx = useContext(TabsContext);
3346
+ if (!ctx) throw new Error("Tabs sub-components must be used inside <Tabs>");
3347
+ return ctx;
3348
+ }
3349
+
3350
+ export function Tabs({
3351
+ value: controlledValue,
3352
+ defaultValue = "",
3353
+ onValueChange,
3354
+ variant = "line",
3355
+ theme,
3356
+ className,
3357
+ children,
3358
+ }: TabsProps) {
3359
+ const { theme: contextTheme } = useBakeThereTheme();
3360
+ const activeTheme = theme ?? contextTheme;
3361
+ const [internalValue, setInternalValue] = useState(defaultValue);
3362
+ const activeValue = controlledValue ?? internalValue;
3363
+
3364
+ const setActiveValue = useCallback(
3365
+ (val: string) => {
3366
+ if (controlledValue === undefined) setInternalValue(val);
3367
+ onValueChange?.(val);
3368
+ },
3369
+ [controlledValue, onValueChange]
3370
+ );
3371
+
3372
+ return (
3373
+ <TabsContext.Provider value={{ activeValue, setActiveValue, variant }}>
3374
+ <div data-bt-theme={activeTheme} className={cn("flex flex-col gap-4", className)}>
3375
+ {children}
3376
+ </div>
3377
+ </TabsContext.Provider>
3378
+ );
3379
+ }
3380
+
3381
+ export function TabsList({ className, children, ...props }: TabsListProps) {
3382
+ const { variant } = useTabsContext();
3383
+ const listRef = useRef<HTMLDivElement>(null);
3384
+
3385
+ const handleKeyDown = useCallback((e: KeyboardEvent<HTMLDivElement>) => {
3386
+ const triggers = listRef.current?.querySelectorAll<HTMLButtonElement>(
3387
+ '[role="tab"]:not([disabled])'
3388
+ );
3389
+ if (!triggers || triggers.length === 0) return;
3390
+ const idx = Array.from(triggers).indexOf(document.activeElement as HTMLButtonElement);
3391
+
3392
+ if (e.key === "ArrowRight") {
3393
+ e.preventDefault();
3394
+ triggers[(idx + 1) % triggers.length].focus();
3395
+ } else if (e.key === "ArrowLeft") {
3396
+ e.preventDefault();
3397
+ triggers[(idx - 1 + triggers.length) % triggers.length].focus();
3398
+ }
3399
+ }, []);
3400
+
3401
+ return (
3402
+ <div
3403
+ ref={listRef}
3404
+ role="tablist"
3405
+ aria-orientation="horizontal"
3406
+ onKeyDown={handleKeyDown}
3407
+ className={cn(
3408
+ "flex items-center",
3409
+ variant === "pill"
3410
+ ? "gap-1 bg-[var(--bt-bg-muted)] p-1 rounded-[var(--bt-radius-md)]"
3411
+ : "border-b border-[var(--bt-border)] gap-0",
3412
+ className
3413
+ )}
3414
+ {...props}
3415
+ >
3416
+ {children}
3417
+ </div>
3418
+ );
3419
+ }
3420
+
3421
+ export function TabsTrigger({
3422
+ value,
3423
+ disabled = false,
3424
+ className,
3425
+ children,
3426
+ onClick,
3427
+ ...props
3428
+ }: TabsTriggerProps) {
3429
+ const { activeValue, setActiveValue, variant } = useTabsContext();
3430
+ const isActive = activeValue === value;
3431
+
3432
+ return (
3433
+ <button
3434
+ role="tab"
3435
+ type="button"
3436
+ aria-selected={isActive}
3437
+ aria-controls={\`tabpanel-\${value}\`}
3438
+ id={\`tab-\${value}\`}
3439
+ disabled={disabled}
3440
+ tabIndex={isActive ? 0 : -1}
3441
+ onClick={(e) => {
3442
+ setActiveValue(value);
3443
+ (onClick as React.MouseEventHandler<HTMLButtonElement>)?.(e);
3444
+ }}
3445
+ className={cn(
3446
+ "inline-flex items-center justify-center px-3 py-2 text-sm font-medium",
3447
+ "transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--bt-ring)]",
3448
+ "disabled:opacity-[var(--bt-disabled-opacity)] disabled:pointer-events-none",
3449
+ variant === "pill"
3450
+ ? cn(
3451
+ "rounded-[var(--bt-radius-sm)]",
3452
+ isActive
3453
+ ? "bg-[var(--bt-bg-elevated)] text-[var(--bt-text-primary)] shadow-[var(--bt-shadow-sm)]"
3454
+ : "text-[var(--bt-text-muted)] hover:text-[var(--bt-text-primary)]"
3455
+ )
3456
+ : cn(
3457
+ "border-b-2 rounded-none -mb-px",
3458
+ isActive
3459
+ ? "border-[var(--bt-accent)] text-[var(--bt-accent)]"
3460
+ : "border-transparent text-[var(--bt-text-muted)] hover:text-[var(--bt-text-primary)]"
3461
+ ),
3462
+ className
3463
+ )}
3464
+ {...(props as React.ButtonHTMLAttributes<HTMLButtonElement>)}
3465
+ >
3466
+ {children}
3467
+ </button>
3468
+ );
3469
+ }
3470
+
3471
+ export function TabsContent({
3472
+ value,
3473
+ className,
3474
+ children,
3475
+ ...props
3476
+ }: TabsContentProps) {
3477
+ const { activeValue } = useTabsContext();
3478
+ if (activeValue !== value) return null;
3479
+
3480
+ return (
3481
+ <div
3482
+ role="tabpanel"
3483
+ id={\`tabpanel-\${value}\`}
3484
+ aria-labelledby={\`tab-\${value}\`}
3485
+ tabIndex={0}
3486
+ className={cn("focus-visible:outline-none animate-[bt-fade-in_0.15s_ease-out]", className)}
3487
+ {...props}
3488
+ >
3489
+ {children}
3490
+ </div>
3491
+ );
3492
+ }
3493
+ `
3494
+ },
3495
+ {
3496
+ name: "tabs.types.ts",
3497
+ content: `import type { HTMLAttributes, ReactNode } from "react";
3498
+ import type { BtTheme } from "../../types";
3499
+
3500
+ export type TabsVariant = "line" | "pill";
3501
+
3502
+ export interface TabsProps {
3503
+ value?: string;
3504
+ defaultValue?: string;
3505
+ onValueChange?: (value: string) => void;
3506
+ variant?: TabsVariant;
3507
+ theme?: BtTheme;
3508
+ className?: string;
3509
+ children: ReactNode;
3510
+ }
3511
+
3512
+ export interface TabsListProps extends HTMLAttributes<HTMLDivElement> {}
3513
+
3514
+ export interface TabsTriggerProps extends HTMLAttributes<HTMLButtonElement> {
3515
+ value: string;
3516
+ disabled?: boolean;
3517
+ }
3518
+
3519
+ export interface TabsContentProps extends HTMLAttributes<HTMLDivElement> {
3520
+ value: string;
3521
+ }
3522
+ `
3523
+ }
3524
+ ]
3525
+ };
3526
+
3527
+ // src/registry/components/accordion.ts
3528
+ var accordion = {
3529
+ name: "accordion",
3530
+ category: "navigation",
3531
+ description: "Collapsible content sections with single/multiple modes",
3532
+ infraDeps: ["tokens", "globals", "provider", "utils"],
3533
+ componentDeps: [],
3534
+ npmDeps: [],
3535
+ files: [
3536
+ {
3537
+ name: "Accordion.tsx",
3538
+ content: `"use client";
3539
+
3540
+ import { createContext, useContext, useState, useCallback } from "react";
3541
+ import { cn } from "@/lib/utils";
3542
+ import { useBakeThereTheme } from "../../provider";
3543
+ import type {
3544
+ AccordionProps,
3545
+ AccordionItemProps,
3546
+ AccordionTriggerProps,
3547
+ AccordionContentProps,
3548
+ } from "./accordion.types";
3549
+
3550
+ interface AccordionContextValue {
3551
+ openValues: string[];
3552
+ toggle: (value: string) => void;
3553
+ type: "single" | "multiple";
3554
+ }
3555
+
3556
+ const AccordionContext = createContext<AccordionContextValue | null>(null);
3557
+
3558
+ function useAccordionContext() {
3559
+ const ctx = useContext(AccordionContext);
3560
+ if (!ctx) throw new Error("Accordion sub-components must be used inside <Accordion>");
3561
+ return ctx;
3562
+ }
3563
+
3564
+ interface AccordionItemContextValue {
3565
+ value: string;
3566
+ isOpen: boolean;
3567
+ }
3568
+
3569
+ const AccordionItemContext = createContext<AccordionItemContextValue | null>(null);
3570
+
3571
+ function useAccordionItemContext() {
3572
+ const ctx = useContext(AccordionItemContext);
3573
+ if (!ctx) throw new Error("AccordionTrigger/AccordionContent must be used inside <AccordionItem>");
3574
+ return ctx;
3575
+ }
3576
+
3577
+ export function Accordion({
3578
+ type = "single",
3579
+ value: controlledValue,
3580
+ defaultValue,
3581
+ onValueChange,
3582
+ theme,
3583
+ className,
3584
+ children,
3585
+ ...props
3586
+ }: AccordionProps) {
3587
+ const { theme: contextTheme } = useBakeThereTheme();
3588
+ const activeTheme = theme ?? contextTheme;
3589
+
3590
+ const getInitial = (): string[] => {
3591
+ if (defaultValue) return Array.isArray(defaultValue) ? defaultValue : [defaultValue];
3592
+ return [];
3593
+ };
3594
+
3595
+ const [internalOpen, setInternalOpen] = useState<string[]>(getInitial);
3596
+ const openValues =
3597
+ controlledValue !== undefined
3598
+ ? Array.isArray(controlledValue)
3599
+ ? controlledValue
3600
+ : [controlledValue]
3601
+ : internalOpen;
3602
+
3603
+ const toggle = useCallback(
3604
+ (val: string) => {
3605
+ let next: string[];
3606
+ if (type === "single") {
3607
+ next = openValues.includes(val) ? [] : [val];
3608
+ } else {
3609
+ next = openValues.includes(val)
3610
+ ? openValues.filter((v) => v !== val)
3611
+ : [...openValues, val];
3612
+ }
3613
+ if (controlledValue === undefined) setInternalOpen(next);
3614
+ onValueChange?.(type === "single" ? (next[0] ?? "") : next);
3615
+ },
3616
+ [openValues, type, controlledValue, onValueChange]
3617
+ );
3618
+
3619
+ return (
3620
+ <AccordionContext.Provider value={{ openValues, toggle, type }}>
3621
+ <div
3622
+ data-bt-theme={activeTheme}
3623
+ className={cn(
3624
+ "divide-y divide-[var(--bt-border)] border border-[var(--bt-border)] rounded-[var(--bt-radius-md)]",
3625
+ className
3626
+ )}
3627
+ {...props}
3628
+ >
3629
+ {children}
3630
+ </div>
3631
+ </AccordionContext.Provider>
3632
+ );
3633
+ }
3634
+
3635
+ export function AccordionItem({ value, className, children, ...props }: AccordionItemProps) {
3636
+ const { openValues } = useAccordionContext();
3637
+ const isOpen = openValues.includes(value);
3638
+ return (
3639
+ <AccordionItemContext.Provider value={{ value, isOpen }}>
3640
+ <div className={cn("overflow-hidden", className)} {...props}>
3641
+ {children}
3642
+ </div>
3643
+ </AccordionItemContext.Provider>
3644
+ );
3645
+ }
3646
+
3647
+ export function AccordionTrigger({ className, children, ...props }: AccordionTriggerProps) {
3648
+ const { toggle } = useAccordionContext();
3649
+ const { value, isOpen } = useAccordionItemContext();
3650
+ return (
3651
+ <button
3652
+ type="button"
3653
+ aria-expanded={isOpen}
3654
+ onClick={() => toggle(value)}
3655
+ className={cn(
3656
+ "flex w-full items-center justify-between px-4 py-3 text-sm font-medium",
3657
+ "text-[var(--bt-text-primary)] hover:bg-[var(--bt-bg-muted)]",
3658
+ "transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--bt-ring)] focus-visible:ring-inset",
3659
+ className
3660
+ )}
3661
+ {...props}
3662
+ >
3663
+ {children}
3664
+ <svg
3665
+ width="16"
3666
+ height="16"
3667
+ viewBox="0 0 24 24"
3668
+ fill="none"
3669
+ stroke="currentColor"
3670
+ strokeWidth="2"
3671
+ strokeLinecap="round"
3672
+ strokeLinejoin="round"
3673
+ style={{
3674
+ transform: isOpen ? "rotate(180deg)" : "rotate(0deg)",
3675
+ transition: "transform 0.2s ease",
3676
+ }}
3677
+ aria-hidden="true"
3678
+ >
3679
+ <polyline points="6 9 12 15 18 9" />
3680
+ </svg>
3681
+ </button>
3682
+ );
3683
+ }
3684
+
3685
+ export function AccordionContent({ className, children, ...props }: AccordionContentProps) {
3686
+ const { isOpen } = useAccordionItemContext();
3687
+ return (
3688
+ <div
3689
+ style={{
3690
+ display: "grid",
3691
+ gridTemplateRows: isOpen ? "1fr" : "0fr",
3692
+ transition: "grid-template-rows 0.2s ease",
3693
+ }}
3694
+ >
3695
+ <div style={{ overflow: "hidden" }}>
3696
+ <div
3697
+ className={cn("px-4 pb-3 pt-0 text-sm text-[var(--bt-text-secondary)]", className)}
3698
+ {...props}
3699
+ >
3700
+ {children}
3701
+ </div>
3702
+ </div>
3703
+ </div>
3704
+ );
3705
+ }
3706
+ `
3707
+ },
3708
+ {
3709
+ name: "accordion.types.ts",
3710
+ content: `import type { HTMLAttributes, ButtonHTMLAttributes } from "react";
3711
+ import type { BtTheme } from "../../types";
3712
+
3713
+ export interface AccordionProps extends HTMLAttributes<HTMLDivElement> {
3714
+ type?: "single" | "multiple";
3715
+ value?: string | string[];
3716
+ defaultValue?: string | string[];
3717
+ onValueChange?: (value: string | string[]) => void;
3718
+ theme?: BtTheme;
3719
+ }
3720
+
3721
+ export interface AccordionItemProps extends HTMLAttributes<HTMLDivElement> {
3722
+ value: string;
3723
+ }
3724
+
3725
+ export interface AccordionTriggerProps extends ButtonHTMLAttributes<HTMLButtonElement> {}
3726
+
3727
+ export interface AccordionContentProps extends HTMLAttributes<HTMLDivElement> {}
3728
+ `
3729
+ }
3730
+ ]
3731
+ };
3732
+
3733
+ // src/registry/components/breadcrumb.ts
3734
+ var breadcrumb = {
3735
+ name: "breadcrumb",
3736
+ category: "navigation",
3737
+ description: "Navigation breadcrumb with separator and current page",
3738
+ infraDeps: ["tokens", "globals", "utils"],
3739
+ componentDeps: [],
3740
+ npmDeps: [],
3741
+ files: [
3742
+ {
3743
+ name: "Breadcrumb.tsx",
3744
+ content: `import { cn } from "@/lib/utils";
3745
+ import type {
3746
+ BreadcrumbProps,
3747
+ BreadcrumbItemProps,
3748
+ BreadcrumbSeparatorProps,
3749
+ } from "./breadcrumb.types";
3750
+
3751
+ export function Breadcrumb({ className, children, ...props }: BreadcrumbProps) {
3752
+ return (
3753
+ <nav aria-label="Breadcrumb" className={cn("", className)} {...props}>
3754
+ <ol className="flex flex-wrap items-center gap-1.5 text-sm text-[var(--bt-text-muted)]">
3755
+ {children}
3756
+ </ol>
3757
+ </nav>
3758
+ );
3759
+ }
3760
+
3761
+ export function BreadcrumbItem({
3762
+ href,
3763
+ isCurrentPage = false,
3764
+ className,
3765
+ children,
3766
+ ...props
3767
+ }: BreadcrumbItemProps) {
3768
+ if (isCurrentPage) {
3769
+ return (
3770
+ <li aria-current="page" className={cn("font-medium text-[var(--bt-text-primary)]", className)}>
3771
+ {children}
3772
+ </li>
3773
+ );
3774
+ }
3775
+ return (
3776
+ <li className={cn("", className)}>
3777
+ <a
3778
+ href={href}
3779
+ className="transition-colors hover:text-[var(--bt-text-primary)]"
3780
+ {...props}
3781
+ >
3782
+ {children}
3783
+ </a>
3784
+ </li>
3785
+ );
3786
+ }
3787
+
3788
+ export function BreadcrumbSeparator({
3789
+ separator = "/",
3790
+ className,
3791
+ ...props
3792
+ }: BreadcrumbSeparatorProps) {
3793
+ return (
3794
+ <li aria-hidden="true" className={cn("text-[var(--bt-text-muted)]", className)} {...props}>
3795
+ {separator}
3796
+ </li>
3797
+ );
3798
+ }
3799
+ `
3800
+ },
3801
+ {
3802
+ name: "breadcrumb.types.ts",
3803
+ content: `import type { HTMLAttributes, AnchorHTMLAttributes } from "react";
3804
+
3805
+ export interface BreadcrumbProps extends HTMLAttributes<HTMLElement> {}
3806
+
3807
+ export interface BreadcrumbItemProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
3808
+ href?: string;
3809
+ isCurrentPage?: boolean;
3810
+ }
3811
+
3812
+ export interface BreadcrumbSeparatorProps extends HTMLAttributes<HTMLLIElement> {
3813
+ separator?: string;
3814
+ }
3815
+ `
3816
+ }
3817
+ ]
3818
+ };
3819
+
3820
+ // src/registry/components/table.ts
3821
+ var table = {
3822
+ name: "table",
3823
+ category: "data",
3824
+ description: "Data table with header, body, and cell styling",
3825
+ infraDeps: ["tokens", "globals", "utils"],
3826
+ componentDeps: [],
3827
+ npmDeps: [],
3828
+ files: [
3829
+ {
3830
+ name: "Table.tsx",
3831
+ content: `import { cn } from "@/lib/utils";
3832
+ import type {
3833
+ TableProps, TableHeaderProps, TableBodyProps, TableFooterProps,
3834
+ TableRowProps, TableHeadProps, TableCellProps, TableCaptionProps,
3835
+ } from "./table.types";
3836
+
3837
+ export function Table({ striped = false, theme, className, children, ...props }: TableProps) {
3838
+ return (
3839
+ <div
3840
+ {...(theme ? { "data-bt-theme": theme } : {})}
3841
+ className="w-full overflow-auto rounded-[var(--bt-radius-md)] border border-[var(--bt-border)]"
3842
+ >
3843
+ <table
3844
+ className={cn(
3845
+ "w-full caption-bottom text-sm text-[var(--bt-text-primary)]",
3846
+ striped && "[&_tbody_tr:nth-child(odd)]:bg-[var(--bt-bg-muted)]",
3847
+ className
3848
+ )}
3849
+ {...props}
3850
+ >
3851
+ {children}
3852
+ </table>
3853
+ </div>
3854
+ );
3855
+ }
3856
+
3857
+ export function TableHeader({ sticky = false, className, children, ...props }: TableHeaderProps) {
3858
+ return (
3859
+ <thead
3860
+ className={cn(
3861
+ "bg-[var(--bt-bg-elevated)] text-[var(--bt-text-secondary)]",
3862
+ sticky && "sticky top-0 z-10",
3863
+ className
3864
+ )}
3865
+ {...props}
3866
+ >
3867
+ {children}
3868
+ </thead>
3869
+ );
3870
+ }
3871
+
3872
+ export function TableBody({ className, children, ...props }: TableBodyProps) {
3873
+ return (
3874
+ <tbody className={cn("divide-y divide-[var(--bt-border)]", className)} {...props}>
3875
+ {children}
3876
+ </tbody>
3877
+ );
3878
+ }
3879
+
3880
+ export function TableFooter({ className, children, ...props }: TableFooterProps) {
3881
+ return (
3882
+ <tfoot
3883
+ className={cn("bg-[var(--bt-bg-elevated)] text-[var(--bt-text-secondary)] font-medium", className)}
3884
+ {...props}
3885
+ >
3886
+ {children}
3887
+ </tfoot>
3888
+ );
3889
+ }
3890
+
3891
+ export function TableRow({ className, children, ...props }: TableRowProps) {
3892
+ return (
3893
+ <tr
3894
+ className={cn(
3895
+ "border-b border-[var(--bt-border)] transition-colors hover:bg-[var(--bt-hover-bg)]",
3896
+ className
3897
+ )}
3898
+ {...props}
3899
+ >
3900
+ {children}
3901
+ </tr>
3902
+ );
3903
+ }
3904
+
3905
+ export function TableHead({ className, children, ...props }: TableHeadProps) {
3906
+ return (
3907
+ <th
3908
+ scope="col"
3909
+ className={cn(
3910
+ "h-10 px-4 text-left align-middle font-semibold text-xs uppercase tracking-wide text-[var(--bt-text-muted)]",
3911
+ className
3912
+ )}
3913
+ {...props}
3914
+ >
3915
+ {children}
3916
+ </th>
3917
+ );
3918
+ }
3919
+
3920
+ export function TableCell({ className, children, ...props }: TableCellProps) {
3921
+ return (
3922
+ <td className={cn("px-4 py-3 align-middle", className)} {...props}>
3923
+ {children}
3924
+ </td>
3925
+ );
3926
+ }
3927
+
3928
+ export function TableCaption({ className, children, ...props }: TableCaptionProps) {
3929
+ return (
3930
+ <caption className={cn("mt-4 text-sm text-[var(--bt-text-muted)] text-center", className)} {...props}>
3931
+ {children}
3932
+ </caption>
3933
+ );
3934
+ }
3935
+ `
3936
+ },
3937
+ {
3938
+ name: "table.types.ts",
3939
+ content: `import type { HTMLAttributes, ThHTMLAttributes, TdHTMLAttributes, TableHTMLAttributes } from "react";
3940
+ import type { BtTheme } from "../../types";
3941
+
3942
+ export interface TableProps extends TableHTMLAttributes<HTMLTableElement> {
3943
+ striped?: boolean;
3944
+ theme?: BtTheme;
3945
+ }
3946
+
3947
+ export interface TableHeaderProps extends HTMLAttributes<HTMLTableSectionElement> {
3948
+ sticky?: boolean;
3949
+ }
3950
+
3951
+ export interface TableBodyProps extends HTMLAttributes<HTMLTableSectionElement> {}
3952
+ export interface TableFooterProps extends HTMLAttributes<HTMLTableSectionElement> {}
3953
+ export interface TableRowProps extends HTMLAttributes<HTMLTableRowElement> {}
3954
+ export interface TableHeadProps extends ThHTMLAttributes<HTMLTableCellElement> {}
3955
+ export interface TableCellProps extends TdHTMLAttributes<HTMLTableCellElement> {}
3956
+ export interface TableCaptionProps extends HTMLAttributes<HTMLTableCaptionElement> {}
3957
+ `
3958
+ }
3959
+ ]
3960
+ };
3961
+
3962
+ // src/registry/components/sidebar.ts
3963
+ var sidebar = {
3964
+ name: "sidebar",
3965
+ category: "layout",
3966
+ description: "Fixed navigation sidebar with overlay backdrop",
3967
+ infraDeps: ["tokens", "globals", "provider", "utils"],
3968
+ componentDeps: [],
3969
+ npmDeps: [],
3970
+ files: [
3971
+ {
3972
+ name: "Sidebar.tsx",
3973
+ content: `"use client";
3974
+
3975
+ import { createContext, useContext, useState, useCallback, useEffect } from "react";
3976
+ import { cn } from "@/lib/utils";
3977
+ import { useBakeThereTheme } from "../../provider";
3978
+ import type {
3979
+ SidebarProps, SidebarTriggerProps, SidebarContentProps,
3980
+ SidebarHeaderProps, SidebarBodyProps, SidebarFooterProps, SidebarCloseProps,
3981
+ } from "./sidebar.types";
3982
+
3983
+ interface SidebarContextValue {
3984
+ open: boolean;
3985
+ onOpenChange: (open: boolean) => void;
3986
+ side: "left" | "right";
3987
+ closeOnBackdrop: boolean;
3988
+ size: "sm" | "md" | "lg";
3989
+ }
3990
+
3991
+ const SidebarContext = createContext<SidebarContextValue | null>(null);
3992
+
3993
+ function useSidebarContext() {
3994
+ const ctx = useContext(SidebarContext);
3995
+ if (!ctx) throw new Error("Sidebar sub-components must be used inside <Sidebar>");
3996
+ return ctx;
3997
+ }
3998
+
3999
+ const sizeWidths = { sm: "240px", md: "288px", lg: "360px" };
4000
+
4001
+ export function Sidebar({
4002
+ open: controlledOpen,
4003
+ onOpenChange,
4004
+ defaultOpen = false,
4005
+ side = "left",
4006
+ closeOnBackdrop = true,
4007
+ size = "md",
4008
+ theme,
4009
+ className,
4010
+ children,
4011
+ }: SidebarProps) {
4012
+ const { theme: contextTheme } = useBakeThereTheme();
4013
+ const activeTheme = theme ?? contextTheme;
4014
+ const [internalOpen, setInternalOpen] = useState(defaultOpen);
4015
+ const open = controlledOpen !== undefined ? controlledOpen : internalOpen;
4016
+
4017
+ const handleOpenChange = useCallback(
4018
+ (val: boolean) => {
4019
+ if (controlledOpen === undefined) setInternalOpen(val);
4020
+ onOpenChange?.(val);
4021
+ },
4022
+ [controlledOpen, onOpenChange]
4023
+ );
4024
+
4025
+ useEffect(() => {
4026
+ if (open) document.body.style.overflow = "hidden";
4027
+ else document.body.style.overflow = "";
4028
+ return () => { document.body.style.overflow = ""; };
4029
+ }, [open]);
4030
+
4031
+ return (
4032
+ <SidebarContext.Provider value={{ open, onOpenChange: handleOpenChange, side, closeOnBackdrop, size }}>
4033
+ <div data-bt-theme={activeTheme} className={className}>
4034
+ {children}
4035
+ </div>
4036
+ </SidebarContext.Provider>
4037
+ );
4038
+ }
4039
+
4040
+ export function SidebarTrigger({ children, onClick, className, ...props }: SidebarTriggerProps) {
4041
+ const { onOpenChange } = useSidebarContext();
4042
+ return (
4043
+ <button
4044
+ type="button"
4045
+ onClick={(e) => { onOpenChange(true); onClick?.(e); }}
4046
+ className={cn(
4047
+ "inline-flex items-center gap-1.5 px-4 py-2 text-sm font-medium",
4048
+ "rounded-[var(--bt-radius-md)] border border-[var(--bt-border)]",
4049
+ "text-[var(--bt-text-primary)] bg-transparent hover:bg-[var(--bt-hover-bg)]",
4050
+ "transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--bt-ring)]",
4051
+ className
4052
+ )}
4053
+ {...props}
4054
+ >
4055
+ {children}
4056
+ </button>
4057
+ );
4058
+ }
4059
+
4060
+ export function SidebarContent({ children, className }: SidebarContentProps) {
4061
+ const { open, onOpenChange, side, closeOnBackdrop, size } = useSidebarContext();
4062
+
4063
+ return (
4064
+ <>
4065
+ {/* Backdrop */}
4066
+ <div
4067
+ aria-hidden="true"
4068
+ onClick={() => closeOnBackdrop && onOpenChange(false)}
4069
+ style={{
4070
+ position: "fixed",
4071
+ inset: 0,
4072
+ zIndex: 40,
4073
+ background: "rgba(0,0,0,0.5)",
4074
+ opacity: open ? 1 : 0,
4075
+ pointerEvents: open ? "auto" : "none",
4076
+ transition: "opacity 0.25s ease",
4077
+ willChange: "opacity",
4078
+ }}
4079
+ />
4080
+ {/* Panel */}
4081
+ <div
4082
+ role="dialog"
4083
+ aria-modal="true"
4084
+ style={{
4085
+ position: "fixed",
4086
+ top: 0,
4087
+ bottom: 0,
4088
+ [side]: 0,
4089
+ width: sizeWidths[size],
4090
+ maxWidth: "90vw",
4091
+ zIndex: 50,
4092
+ transform: open
4093
+ ? "translateX(0)"
4094
+ : side === "left"
4095
+ ? "translateX(-100%)"
4096
+ : "translateX(100%)",
4097
+ transition: "transform 0.3s cubic-bezier(0.4, 0, 0.2, 1)",
4098
+ willChange: "transform",
4099
+ }}
4100
+ className={cn(
4101
+ "flex flex-col",
4102
+ "bg-[var(--bt-bg-surface)]",
4103
+ side === "left" ? "border-r border-[var(--bt-border)]" : "border-l border-[var(--bt-border)]",
4104
+ "shadow-[var(--bt-shadow-lg)]",
4105
+ className
4106
+ )}
4107
+ >
4108
+ {children}
4109
+ </div>
4110
+ </>
4111
+ );
4112
+ }
4113
+
4114
+ export function SidebarHeader({ children, className, ...props }: SidebarHeaderProps) {
4115
+ return (
4116
+ <div
4117
+ className={cn("flex items-center justify-between px-4 py-3 border-b border-[var(--bt-border)] shrink-0", className)}
4118
+ {...props}
4119
+ >
4120
+ {children}
4121
+ </div>
4122
+ );
4123
+ }
4124
+
4125
+ export function SidebarBody({ children, className, ...props }: SidebarBodyProps) {
4126
+ return (
4127
+ <div className={cn("flex-1 overflow-y-auto px-4 py-3", className)} {...props}>
4128
+ {children}
4129
+ </div>
4130
+ );
4131
+ }
4132
+
4133
+ export function SidebarFooter({ children, className, ...props }: SidebarFooterProps) {
4134
+ return (
4135
+ <div className={cn("shrink-0 px-4 py-3 border-t border-[var(--bt-border)]", className)} {...props}>
4136
+ {children}
4137
+ </div>
4138
+ );
4139
+ }
4140
+
4141
+ export function SidebarClose({ children, onClick, className, ...props }: SidebarCloseProps) {
4142
+ const { onOpenChange } = useSidebarContext();
4143
+ return (
4144
+ <button
4145
+ type="button"
4146
+ aria-label="Close sidebar"
4147
+ onClick={(e) => { onOpenChange(false); onClick?.(e); }}
4148
+ className={cn(
4149
+ "inline-flex items-center justify-center rounded-[var(--bt-radius-sm)]",
4150
+ "w-8 h-8 text-[var(--bt-text-muted)] hover:text-[var(--bt-text-primary)]",
4151
+ "hover:bg-[var(--bt-hover-bg)] transition-colors",
4152
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--bt-ring)]",
4153
+ className
4154
+ )}
4155
+ {...props}
4156
+ >
4157
+ {children ?? (
4158
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
4159
+ <line x1="18" y1="6" x2="6" y2="18" />
4160
+ <line x1="6" y1="6" x2="18" y2="18" />
4161
+ </svg>
4162
+ )}
4163
+ </button>
4164
+ );
4165
+ }
4166
+ `
4167
+ },
4168
+ {
4169
+ name: "sidebar.types.ts",
4170
+ content: `import type { HTMLAttributes, ButtonHTMLAttributes, ReactNode } from "react";
4171
+ import type { BtTheme } from "../../types";
4172
+
4173
+ export interface SidebarProps {
4174
+ open?: boolean;
4175
+ onOpenChange?: (open: boolean) => void;
4176
+ defaultOpen?: boolean;
4177
+ side?: "left" | "right";
4178
+ closeOnBackdrop?: boolean;
4179
+ size?: "sm" | "md" | "lg";
4180
+ theme?: BtTheme;
4181
+ className?: string;
4182
+ children: ReactNode;
4183
+ }
4184
+
4185
+ export interface SidebarTriggerProps extends ButtonHTMLAttributes<HTMLButtonElement> {}
4186
+ export interface SidebarContentProps extends HTMLAttributes<HTMLDivElement> {}
4187
+ export interface SidebarHeaderProps extends HTMLAttributes<HTMLDivElement> {}
4188
+ export interface SidebarBodyProps extends HTMLAttributes<HTMLDivElement> {}
4189
+ export interface SidebarFooterProps extends HTMLAttributes<HTMLDivElement> {}
4190
+ export interface SidebarCloseProps extends ButtonHTMLAttributes<HTMLButtonElement> {}
4191
+ `
4192
+ }
4193
+ ]
4194
+ };
4195
+
4196
+ // src/registry/components/header.ts
4197
+ var header = {
4198
+ name: "header",
4199
+ category: "layout",
4200
+ description: "Page header with navigation and action slots",
4201
+ infraDeps: ["tokens", "globals", "provider", "utils"],
4202
+ componentDeps: [],
4203
+ npmDeps: [],
4204
+ files: [
4205
+ {
4206
+ name: "Header.tsx",
4207
+ content: `"use client";
4208
+
4209
+ import { useState, useEffect, useRef } from "react";
4210
+ import { cn } from "@/lib/utils";
4211
+ import { useBakeThereTheme } from "../../provider";
4212
+ import type { HeaderProps, HeaderBrandProps, HeaderNavProps, HeaderActionsProps } from "./header.types";
4213
+
4214
+ export function Header({
4215
+ sticky = false,
4216
+ variant = "bordered",
4217
+ scrollFade = false,
4218
+ theme,
4219
+ className,
4220
+ children,
4221
+ ...props
4222
+ }: HeaderProps) {
4223
+ const { theme: contextTheme } = useBakeThereTheme();
4224
+ const activeTheme = theme ?? contextTheme;
4225
+ const [scrolled, setScrolled] = useState(false);
4226
+ const headerRef = useRef<HTMLElement>(null);
4227
+
4228
+ useEffect(() => {
4229
+ const el = headerRef.current;
4230
+ if (!scrollFade || !el) return;
4231
+
4232
+ // Walk up the DOM to find the nearest scrollable ancestor.
4233
+ // Terminates when el becomes null (reached top of the tree without finding one).
4234
+ let scrollEl: Element | null = el.parentElement;
4235
+ while (scrollEl) {
4236
+ const { overflowY } = window.getComputedStyle(scrollEl);
4237
+ // "overlay" is a legacy WebKit alias for "scroll"; harmless to include.
4238
+ if (overflowY === "auto" || overflowY === "scroll" || overflowY === "overlay") break;
4239
+ scrollEl = scrollEl.parentElement;
4240
+ }
4241
+
4242
+ // Use found container, or fall back to window (e.g. full-page layouts).
4243
+ const target: Element | Window = scrollEl ?? window;
4244
+
4245
+ const getScrollTop = () =>
4246
+ target instanceof Window ? target.scrollY : (target as Element).scrollTop;
4247
+
4248
+ const handle = () => setScrolled(getScrollTop() > 20);
4249
+
4250
+ // Initialize immediately \u2014 container may already be scrolled on mount.
4251
+ handle();
4252
+
4253
+ target.addEventListener("scroll", handle, { passive: true });
4254
+ return () => target.removeEventListener("scroll", handle);
4255
+ }, [scrollFade]);
4256
+
4257
+ const visible = !scrollFade || scrolled;
4258
+
4259
+ return (
4260
+ <header
4261
+ ref={headerRef}
4262
+ data-bt-theme={activeTheme}
4263
+ style={scrollFade ? { transition: "background 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease" } : undefined}
4264
+ className={cn(
4265
+ "w-full z-30",
4266
+ sticky ? "sticky top-0" : "relative",
4267
+ visible ? "bg-[var(--bt-bg-base)]" : "bg-transparent",
4268
+ variant === "bordered" && visible && "border-b border-[var(--bt-border)]",
4269
+ variant === "elevated" && visible && "shadow-[var(--bt-shadow-sm)]",
4270
+ className
4271
+ )}
4272
+ {...props}
4273
+ >
4274
+ <div className="flex items-center h-14 px-4 gap-4">
4275
+ {children}
4276
+ </div>
4277
+ </header>
4278
+ );
4279
+ }
4280
+
4281
+ export function HeaderBrand({ className, children, ...props }: HeaderBrandProps) {
4282
+ return (
4283
+ <div className={cn("flex items-center gap-2 shrink-0", className)} {...props}>
4284
+ {children}
4285
+ </div>
4286
+ );
4287
+ }
4288
+
4289
+ export function HeaderNav({ className, children, ...props }: HeaderNavProps) {
4290
+ return (
4291
+ <nav className={cn("flex-1 flex items-center gap-1", className)} {...props}>
4292
+ {children}
4293
+ </nav>
4294
+ );
4295
+ }
4296
+
4297
+ export function HeaderActions({ className, children, ...props }: HeaderActionsProps) {
4298
+ return (
4299
+ <div className={cn("flex items-center gap-2 ml-auto shrink-0", className)} {...props}>
4300
+ {children}
4301
+ </div>
4302
+ );
4303
+ }
4304
+ `
4305
+ },
4306
+ {
4307
+ name: "header.types.ts",
4308
+ content: `import type { HTMLAttributes } from "react";
4309
+ import type { BtTheme } from "../../types";
4310
+
4311
+ export interface HeaderProps extends HTMLAttributes<HTMLElement> {
4312
+ sticky?: boolean;
4313
+ variant?: "flat" | "bordered" | "elevated";
4314
+ scrollFade?: boolean;
4315
+ theme?: BtTheme;
4316
+ }
4317
+
4318
+ export interface HeaderBrandProps extends HTMLAttributes<HTMLDivElement> {}
4319
+ export interface HeaderNavProps extends HTMLAttributes<HTMLElement> {}
4320
+ export interface HeaderActionsProps extends HTMLAttributes<HTMLDivElement> {}
4321
+ `
4322
+ }
4323
+ ]
4324
+ };
4325
+
4326
+ // src/registry/index.ts
4327
+ var registry = {
4328
+ button,
4329
+ "button-group": buttonGroup,
4330
+ input,
4331
+ label,
4332
+ checkbox,
4333
+ toggle,
4334
+ text,
4335
+ textarea,
4336
+ radio,
4337
+ slider,
4338
+ select,
4339
+ card,
4340
+ badge,
4341
+ avatar,
4342
+ separator,
4343
+ skeleton,
4344
+ stat,
4345
+ empty,
4346
+ alert,
4347
+ progress,
4348
+ dialog,
4349
+ tooltip,
4350
+ toast,
4351
+ popover,
4352
+ "dropdown-menu": dropdownMenu,
4353
+ tabs,
4354
+ accordion,
4355
+ breadcrumb,
4356
+ table,
4357
+ sidebar,
4358
+ header
4359
+ };
4360
+ var categories = {
4361
+ primitives: ["button", "button-group", "input", "label", "checkbox", "toggle", "text", "textarea", "radio", "slider", "select"],
4362
+ display: ["card", "badge", "avatar", "separator", "skeleton", "stat", "empty", "alert", "progress"],
4363
+ overlay: ["dialog", "tooltip", "toast", "popover"],
4364
+ navigation: ["dropdown-menu", "tabs", "accordion", "breadcrumb"],
4365
+ data: ["table"],
4366
+ layout: ["sidebar", "header"]
4367
+ };
4368
+
4369
+ // src/commands/list.ts
4370
+ var listCommand = new import_commander.Command("list").description("List all available BakeThere components").action(() => {
4371
+ (0, import_prompts.intro)("BakeThere Components");
4372
+ for (const [category, names] of Object.entries(categories)) {
4373
+ const label2 = category.charAt(0).toUpperCase() + category.slice(1);
4374
+ import_prompts.log.message(`${label2.padEnd(12)} ${names.join(" ")}`);
4375
+ }
4376
+ (0, import_prompts.outro)("Run 'bakethere add <name>' to add a component.");
4377
+ });
4378
+
4379
+ // src/commands/init.ts
4380
+ var import_commander2 = require("commander");
4381
+ var import_prompts2 = require("@clack/prompts");
4382
+ var import_path4 = require("path");
4383
+ var import_fs4 = require("fs");
4384
+
4385
+ // src/lib/config.ts
4386
+ var import_fs = require("fs");
4387
+ var import_path = require("path");
4388
+ var CONFIG_FILE = "bakethere.json";
4389
+ function hasConfig(cwd) {
4390
+ return (0, import_fs.existsSync)((0, import_path.join)(cwd, CONFIG_FILE));
4391
+ }
4392
+ function readConfig(cwd) {
4393
+ const configPath = (0, import_path.join)(cwd, CONFIG_FILE);
4394
+ if (!(0, import_fs.existsSync)(configPath)) {
4395
+ throw new Error(`bakethere.json not found at ${configPath}. Run 'bakethere init' first.`);
4396
+ }
4397
+ return JSON.parse((0, import_fs.readFileSync)(configPath, "utf-8"));
4398
+ }
4399
+ function writeConfig(cwd, config) {
4400
+ (0, import_fs.writeFileSync)((0, import_path.join)(cwd, CONFIG_FILE), JSON.stringify(config, null, 2) + "\n");
4401
+ }
4402
+
4403
+ // src/lib/detect.ts
4404
+ var import_fs2 = require("fs");
4405
+ var import_path2 = require("path");
4406
+ function detectProject(cwd) {
4407
+ return {
4408
+ hasSrcDir: (0, import_fs2.existsSync)((0, import_path2.join)(cwd, "src")),
4409
+ hasTypeScript: (0, import_fs2.existsSync)((0, import_path2.join)(cwd, "tsconfig.json")),
4410
+ hasUtils: (0, import_fs2.existsSync)((0, import_path2.join)(cwd, "src/lib/utils.ts")) || (0, import_fs2.existsSync)((0, import_path2.join)(cwd, "lib/utils.ts"))
4411
+ };
4412
+ }
4413
+
4414
+ // src/lib/copy.ts
4415
+ var import_fs3 = require("fs");
4416
+ var import_path3 = require("path");
4417
+ function copyFiles(files, targetDir, options = {}) {
4418
+ const results = [];
4419
+ for (const file of files) {
4420
+ const dest = (0, import_path3.join)(targetDir, file.name);
4421
+ if ((0, import_fs3.existsSync)(dest) && !options.overwrite) {
4422
+ results.push({ path: dest, skipped: true });
4423
+ continue;
4424
+ }
4425
+ try {
4426
+ (0, import_fs3.mkdirSync)((0, import_path3.dirname)(dest), { recursive: true });
4427
+ (0, import_fs3.writeFileSync)(dest, file.content, "utf-8");
4428
+ results.push({ path: dest, skipped: false });
4429
+ } catch (e) {
4430
+ results.push({ path: dest, skipped: false, error: String(e) });
4431
+ }
4432
+ }
4433
+ return results;
4434
+ }
4435
+
4436
+ // src/registry/infra.ts
4437
+ var tokensCssContent = `[data-bt-theme="dark"] {
4438
+ /* Backgrounds */
4439
+ --bt-bg-base: #15151E;
4440
+ --bt-bg-surface: #2A2B3C;
4441
+ --bt-bg-elevated: #232336;
4442
+ --bt-bg-overlay: #1E1E2E;
4443
+ --bt-bg-muted: #28283A;
4444
+
4445
+ /* Text */
4446
+ --bt-text-primary: #EDEDED;
4447
+ --bt-text-secondary:#ADADBF;
4448
+ --bt-text-muted: #7E7E99;
4449
+ --bt-text-inverse: #15151E;
4450
+
4451
+ /* Accent (teal) */
4452
+ --bt-accent: #5EDEC6;
4453
+ --bt-accent-hover: #4DCCB3;
4454
+ --bt-accent-muted: rgba(94,222,198,0.15);
4455
+
4456
+ /* Borders */
4457
+ --bt-border: #393951;
4458
+ --bt-border-focus: #5EDEC6;
4459
+ --bt-border-muted: #2F2F44;
4460
+
4461
+ /* Ring */
4462
+ --bt-ring: rgba(94,222,198,0.45);
4463
+
4464
+ /* Semantic */
4465
+ --bt-destructive: #F87171;
4466
+ --bt-destructive-bg: rgba(248,113,113,0.15);
4467
+ --bt-success: #4ADE80;
4468
+ --bt-warning: #FBBF24;
4469
+
4470
+ /* Interactive */
4471
+ --bt-hover-bg: rgba(255,255,255,0.05);
4472
+ --bt-active-bg: rgba(94,222,198,0.12);
4473
+ --bt-disabled-opacity: 0.4;
4474
+
4475
+ /* Shadows */
4476
+ --bt-shadow-sm: 0 1px 3px rgba(0,0,0,0.5);
4477
+ --bt-shadow-md: 0 4px 12px rgba(0,0,0,0.6);
4478
+ --bt-shadow-lg: 0 8px 32px rgba(0,0,0,0.7);
4479
+
4480
+ /* Skeleton */
4481
+ --bt-skeleton-base: #252535;
4482
+ --bt-skeleton-highlight: #393951;
4483
+
4484
+ /* Radius */
4485
+ --bt-radius-sm: 0.25rem;
4486
+ --bt-radius-md: 0.5rem;
4487
+ --bt-radius-lg: 0.75rem;
4488
+ --bt-radius-full: 9999px;
4489
+ }
4490
+
4491
+ [data-bt-theme="warm"] {
4492
+ /* Backgrounds \u2014 sourdough crumb interior: warm cream with blue undertones */
4493
+ --bt-bg-base: #EEF0F8;
4494
+ --bt-bg-surface: #F4F2FB;
4495
+ --bt-bg-elevated: #F9F8FE;
4496
+ --bt-bg-overlay: #EAECF5;
4497
+ --bt-bg-muted: #E5E4F2;
4498
+
4499
+ /* Text \u2014 dark navy to medium slate */
4500
+ --bt-text-primary: #1E2140;
4501
+ --bt-text-secondary:#484E6E;
4502
+ --bt-text-muted: #8890B0;
4503
+ --bt-text-inverse: #FFFFFF;
4504
+
4505
+ /* Accent \u2014 dusty periwinkle blue */
4506
+ --bt-accent: #5573B8;
4507
+ --bt-accent-hover: #4260A3;
4508
+ --bt-accent-muted: rgba(85,115,184,0.15);
4509
+
4510
+ /* Borders \u2014 soft blue-grey */
4511
+ --bt-border: #BFC5DC;
4512
+ --bt-border-focus: #5573B8;
4513
+ --bt-border-muted: #D4D8EC;
4514
+ --bt-ring: rgba(85,115,184,0.4);
4515
+
4516
+ /* Semantic */
4517
+ --bt-destructive: #C44B4B;
4518
+ --bt-destructive-bg: rgba(196,75,75,0.1);
4519
+ --bt-success: #3A8F5A;
4520
+ --bt-warning: #A07020;
4521
+
4522
+ /* Interactive */
4523
+ --bt-hover-bg: rgba(85,115,184,0.06);
4524
+ --bt-active-bg: rgba(85,115,184,0.12);
4525
+ --bt-disabled-opacity: 0.4;
4526
+
4527
+ /* Shadows */
4528
+ --bt-shadow-sm: 0 1px 3px rgba(30,33,64,0.1);
4529
+ --bt-shadow-md: 0 4px 12px rgba(30,33,64,0.12);
4530
+ --bt-shadow-lg: 0 8px 32px rgba(30,33,64,0.16);
4531
+
4532
+ /* Skeleton */
4533
+ --bt-skeleton-base: #D4D8EC;
4534
+ --bt-skeleton-highlight: #F4F2FB;
4535
+
4536
+ /* Radius */
4537
+ --bt-radius-sm: 0.375rem;
4538
+ --bt-radius-md: 0.625rem;
4539
+ --bt-radius-lg: 1rem;
4540
+ --bt-radius-full: 9999px;
4541
+ }
4542
+
4543
+ [data-bt-theme="plain"] {
4544
+ --bt-bg-base: #FFFFFF;
4545
+ --bt-bg-surface: #F9F9F9;
4546
+ --bt-bg-elevated: #F2F2F2;
4547
+ --bt-bg-overlay: #FAFAFA;
4548
+ --bt-bg-muted: #EEEEEE;
4549
+
4550
+ --bt-text-primary: #0A0A0A;
4551
+ --bt-text-secondary:#404040;
4552
+ --bt-text-muted: #888888;
4553
+ --bt-text-inverse: #FFFFFF;
4554
+
4555
+ --bt-accent: #0A0A0A;
4556
+ --bt-accent-hover: #333333;
4557
+ --bt-accent-muted: #E8E8E8;
4558
+
4559
+ --bt-border: #C0C0C0;
4560
+ --bt-border-focus: #0A0A0A;
4561
+ --bt-border-muted: #D8D8D8;
4562
+ --bt-ring: rgba(10,10,10,0.25);
4563
+
4564
+ --bt-destructive: #DC2626;
4565
+ --bt-destructive-bg: rgba(220,38,38,0.12);
4566
+ --bt-success: #16A34A;
4567
+ --bt-warning: #CA8A04;
4568
+
4569
+ --bt-hover-bg: rgba(0,0,0,0.04);
4570
+ --bt-active-bg: rgba(0,0,0,0.08);
4571
+ --bt-disabled-opacity: 0.55;
4572
+
4573
+ --bt-shadow-sm: 0 1px 3px rgba(0,0,0,0.14);
4574
+ --bt-shadow-md: 0 4px 12px rgba(0,0,0,0.12);
4575
+ --bt-shadow-lg: 0 8px 32px rgba(0,0,0,0.15);
4576
+
4577
+ /* Skeleton */
4578
+ --bt-skeleton-base: #D4D4D4;
4579
+ --bt-skeleton-highlight: #F5F5F5;
4580
+
4581
+ --bt-radius-sm: 0.25rem;
4582
+ --bt-radius-md: 0.375rem;
4583
+ --bt-radius-lg: 0.5rem;
4584
+ --bt-radius-full: 9999px;
4585
+ }
4586
+ `;
4587
+ var bakethereGlobalsContent = `/* BakeThere Global Styles \u2014 import in your globals.css */
4588
+
4589
+ ::backdrop { background: rgba(0,0,0,0.5); backdrop-filter: blur(4px); }
4590
+ dialog { position: fixed; inset: 0; margin: auto; }
4591
+
4592
+ [data-bt-tooltip] {
4593
+ position: absolute;
4594
+ z-index: 50;
4595
+ opacity: 0;
4596
+ pointer-events: none;
4597
+ background: var(--bt-bg-elevated);
4598
+ color: var(--bt-text-primary);
4599
+ padding: 0.375rem 0.625rem;
4600
+ border-radius: var(--bt-radius-sm);
4601
+ font-size: 0.75rem;
4602
+ white-space: nowrap;
4603
+ transition: opacity 0.15s;
4604
+ }
4605
+
4606
+ .bt-tooltip-wrapper:hover [data-bt-tooltip],
4607
+ .bt-tooltip-wrapper:focus-within [data-bt-tooltip] {
4608
+ opacity: 1;
4609
+ }
4610
+
4611
+ [data-bt-tooltip][data-side="top"] { bottom: calc(100% + 6px); left: 50%; transform: translateX(-50%); }
4612
+ [data-bt-tooltip][data-side="bottom"] { top: calc(100% + 6px); left: 50%; transform: translateX(-50%); }
4613
+ [data-bt-tooltip][data-side="left"] { right: calc(100% + 6px); top: 50%; transform: translateY(-50%); }
4614
+ [data-bt-tooltip][data-side="right"] { left: calc(100% + 6px); top: 50%; transform: translateY(-50%); }
4615
+
4616
+ /* Themed scrollbar */
4617
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
4618
+ ::-webkit-scrollbar-track { background: transparent; }
4619
+ ::-webkit-scrollbar-thumb { background: var(--bt-border, #ccc); border-radius: 9999px; }
4620
+ ::-webkit-scrollbar-thumb:hover { background: var(--bt-text-muted, #999); }
4621
+ * { scrollbar-width: thin; scrollbar-color: var(--bt-border, #ccc) transparent; }
4622
+
4623
+ /* ButtonGroup */
4624
+ .bt-button-group {
4625
+ display: inline-flex;
4626
+ }
4627
+ .bt-button-group > button:not(:first-child) {
4628
+ margin-left: -1px;
4629
+ border-top-left-radius: 0;
4630
+ border-bottom-left-radius: 0;
4631
+ }
4632
+ .bt-button-group > button:not(:last-child) {
4633
+ border-top-right-radius: 0;
4634
+ border-bottom-right-radius: 0;
4635
+ }
4636
+
4637
+ @keyframes bt-toast-in {
4638
+ from {
4639
+ transform: translateY(1rem);
4640
+ opacity: 0;
4641
+ }
4642
+ to {
4643
+ transform: translateY(0);
4644
+ opacity: 1;
4645
+ }
4646
+ }
4647
+
4648
+ @keyframes bt-dropdown-in {
4649
+ from {
4650
+ transform: translateY(-0.25rem);
4651
+ opacity: 0;
4652
+ }
4653
+ to {
4654
+ transform: translateY(0);
4655
+ opacity: 1;
4656
+ }
4657
+ }
4658
+
4659
+ @keyframes bt-dialog-in {
4660
+ from { transform: scale(0.96) translateY(-6px); opacity: 0; }
4661
+ to { transform: scale(1) translateY(0); opacity: 1; }
4662
+ }
4663
+
4664
+ @keyframes bt-fade-in {
4665
+ from { opacity: 0; transform: translateY(4px); }
4666
+ to { opacity: 1; transform: translateY(0); }
4667
+ }
4668
+
4669
+ @keyframes bt-progress-indeterminate {
4670
+ 0% { transform: translateX(-100%) scaleX(0.5); }
4671
+ 50% { transform: translateX(100%) scaleX(0.8); }
4672
+ 100% { transform: translateX(300%) scaleX(0.5); }
4673
+ }
4674
+
4675
+ /* Slider */
4676
+ input[type="range"].bt-slider {
4677
+ -webkit-appearance: none;
4678
+ appearance: none;
4679
+ width: 100%;
4680
+ height: 6px;
4681
+ border-radius: 9999px;
4682
+ background: linear-gradient(
4683
+ to right,
4684
+ var(--bt-accent) 0%,
4685
+ var(--bt-accent) var(--bt-slider-value, 0%),
4686
+ var(--bt-bg-muted) var(--bt-slider-value, 0%),
4687
+ var(--bt-bg-muted) 100%
4688
+ );
4689
+ outline: none;
4690
+ cursor: pointer;
4691
+ }
4692
+ input[type="range"].bt-slider::-webkit-slider-thumb {
4693
+ -webkit-appearance: none;
4694
+ appearance: none;
4695
+ width: 18px;
4696
+ height: 18px;
4697
+ border-radius: 50%;
4698
+ background: var(--bt-accent);
4699
+ cursor: pointer;
4700
+ box-shadow: var(--bt-shadow-sm);
4701
+ border: 2px solid var(--bt-bg-elevated);
4702
+ }
4703
+ input[type="range"].bt-slider::-moz-range-thumb {
4704
+ width: 18px;
4705
+ height: 18px;
4706
+ border-radius: 50%;
4707
+ background: var(--bt-accent);
4708
+ cursor: pointer;
4709
+ box-shadow: var(--bt-shadow-sm);
4710
+ border: none;
4711
+ }
4712
+ input[type="range"].bt-slider:disabled {
4713
+ opacity: var(--bt-disabled-opacity);
4714
+ pointer-events: none;
4715
+ }
4716
+
4717
+ @keyframes bt-shimmer {
4718
+ from { background-position: -200% 0; }
4719
+ to { background-position: 200% 0; }
4720
+ }
4721
+ `;
4722
+ var providerContent = `"use client";
4723
+
4724
+ import { createContext, useContext, useState, type ReactNode } from "react";
4725
+
4726
+ export type BtTheme = "dark" | "warm" | "plain";
4727
+ // BtBaseProps is co-located here so component files can import it from BakeThereProvider
4728
+ // rather than needing a separate types file in the user's project.
4729
+ export type BtBaseProps = { theme?: BtTheme; className?: string; };
4730
+
4731
+ interface BakeThereContextValue {
4732
+ theme: BtTheme;
4733
+ setTheme: (theme: BtTheme) => void;
4734
+ }
4735
+
4736
+ const BakeThereContext = createContext<BakeThereContextValue | null>(null);
4737
+
4738
+ interface BakeThereProviderProps {
4739
+ children: ReactNode;
4740
+ theme?: BtTheme;
4741
+ defaultTheme?: BtTheme;
4742
+ }
4743
+
4744
+ export function BakeThereProvider({
4745
+ children,
4746
+ theme: controlledTheme,
4747
+ defaultTheme = "dark",
4748
+ }: BakeThereProviderProps) {
4749
+ const [internalTheme, setInternalTheme] = useState<BtTheme>(defaultTheme);
4750
+
4751
+ const activeTheme = controlledTheme ?? internalTheme;
4752
+ const setTheme = controlledTheme !== undefined ? () => {} : setInternalTheme;
4753
+
4754
+ return (
4755
+ <BakeThereContext.Provider value={{ theme: activeTheme, setTheme }}>
4756
+ <div data-bt-theme={activeTheme}>
4757
+ {children}
4758
+ </div>
4759
+ </BakeThereContext.Provider>
4760
+ );
4761
+ }
4762
+
4763
+ export function useBakeThereTheme(): BakeThereContextValue {
4764
+ const ctx = useContext(BakeThereContext);
4765
+ if (!ctx) {
4766
+ throw new Error("useBakeThereTheme must be used within a BakeThereProvider");
4767
+ }
4768
+ return ctx;
4769
+ }
4770
+
4771
+ export function useThemeProps(): { "data-bt-theme": BtTheme } {
4772
+ const { theme } = useBakeThereTheme();
4773
+ return { "data-bt-theme": theme };
4774
+ }
4775
+ `;
4776
+ var utilsContent = `import { clsx, type ClassValue } from "clsx";
4777
+ import { twMerge } from "tailwind-merge";
4778
+
4779
+ export function cn(...inputs: ClassValue[]) {
4780
+ return twMerge(clsx(inputs));
4781
+ }
4782
+ `;
4783
+ var infra = {
4784
+ tokens: { name: "bakethere-tokens.css", content: tokensCssContent },
4785
+ globals: { name: "bakethere-globals.css", content: bakethereGlobalsContent },
4786
+ provider: { name: "BakeThereProvider.tsx", content: providerContent },
4787
+ utils: { name: "utils.ts", content: utilsContent }
4788
+ };
4789
+
4790
+ // src/commands/init.ts
4791
+ var initCommand = new import_commander2.Command("init").description("Initialize BakeThere in your project").option("-y, --yes", "Skip prompts and use defaults", false).action(async (options) => {
4792
+ const cwd = process.cwd();
4793
+ (0, import_prompts2.intro)("BakeThere Init");
4794
+ if (hasConfig(cwd)) {
4795
+ import_prompts2.log.warn("bakethere.json already exists. Remove it to re-initialize.");
4796
+ process.exit(0);
4797
+ }
4798
+ const detected = detectProject(cwd);
4799
+ const defaultComponentsDir = detected.hasSrcDir ? "src/components/ui" : "components/ui";
4800
+ const defaultStylesDir = detected.hasSrcDir ? "src/styles" : "styles";
4801
+ let componentsDir = defaultComponentsDir;
4802
+ let componentsAlias = "@/components/ui";
4803
+ let stylesDir = defaultStylesDir;
4804
+ let utilsAlias = "@/lib/utils";
4805
+ if (!options.yes) {
4806
+ const inputComponentsDir = await (0, import_prompts2.text)({
4807
+ message: "Where should components be installed?",
4808
+ placeholder: defaultComponentsDir,
4809
+ defaultValue: defaultComponentsDir
4810
+ });
4811
+ if (typeof inputComponentsDir === "string") componentsDir = inputComponentsDir;
4812
+ const inputAlias = await (0, import_prompts2.text)({
4813
+ message: "What is the TypeScript alias for the components directory?",
4814
+ placeholder: "@/components/ui",
4815
+ defaultValue: "@/components/ui"
4816
+ });
4817
+ if (typeof inputAlias === "string") componentsAlias = inputAlias;
4818
+ const inputStylesDir = await (0, import_prompts2.text)({
4819
+ message: "Where should styles be installed?",
4820
+ placeholder: defaultStylesDir,
4821
+ defaultValue: defaultStylesDir
4822
+ });
4823
+ if (typeof inputStylesDir === "string") stylesDir = inputStylesDir;
4824
+ const inputUtils = await (0, import_prompts2.text)({
4825
+ message: "What is your utils import alias?",
4826
+ placeholder: "@/lib/utils",
4827
+ defaultValue: "@/lib/utils"
4828
+ });
4829
+ if (typeof inputUtils === "string") utilsAlias = inputUtils;
4830
+ }
4831
+ const config = {
4832
+ componentsDir,
4833
+ componentsAlias,
4834
+ stylesDir,
4835
+ aliases: { utils: utilsAlias }
4836
+ };
4837
+ const s1 = (0, import_prompts2.spinner)();
4838
+ s1.start("Writing bakethere.json");
4839
+ try {
4840
+ writeConfig(cwd, config);
4841
+ } catch (err) {
4842
+ s1.stop("Failed to write bakethere.json");
4843
+ import_prompts2.log.error(String(err));
4844
+ process.exit(1);
4845
+ }
4846
+ s1.stop("bakethere.json created");
4847
+ (0, import_fs4.mkdirSync)((0, import_path4.join)(cwd, stylesDir), { recursive: true });
4848
+ (0, import_fs4.mkdirSync)((0, import_path4.join)(cwd, componentsDir), { recursive: true });
4849
+ const stylesFiles = [infra.tokens, infra.globals];
4850
+ const componentFiles = [infra.provider];
4851
+ const utilsFiles = detected.hasUtils ? [] : [infra.utils];
4852
+ const utilsDir = detected.hasSrcDir ? "src/lib" : "lib";
4853
+ const s2 = (0, import_prompts2.spinner)();
4854
+ s2.start("Copying infrastructure files");
4855
+ let infraErrors = false;
4856
+ const allInfraResults = [
4857
+ ...copyFiles(stylesFiles, (0, import_path4.join)(cwd, stylesDir)),
4858
+ ...copyFiles(componentFiles, (0, import_path4.join)(cwd, componentsDir)),
4859
+ ...utilsFiles.length > 0 ? (() => {
4860
+ (0, import_fs4.mkdirSync)((0, import_path4.join)(cwd, utilsDir), { recursive: true });
4861
+ return copyFiles(utilsFiles, (0, import_path4.join)(cwd, utilsDir));
4862
+ })() : []
4863
+ ];
4864
+ s2.stop("Infrastructure files copied");
4865
+ for (const r of allInfraResults) {
4866
+ if (r.error) {
4867
+ import_prompts2.log.error(`Failed to write ${r.path}: ${r.error}`);
4868
+ infraErrors = true;
4869
+ }
4870
+ }
4871
+ if (infraErrors) {
4872
+ (0, import_prompts2.outro)("Done with errors. Some infrastructure files may not have been written.");
4873
+ process.exit(1);
4874
+ }
4875
+ (0, import_prompts2.outro)(`Done! Next steps:
4876
+ 1. Add to your globals.css:
4877
+ @import '${stylesDir}/bakethere-tokens.css';
4878
+ @import '${stylesDir}/bakethere-globals.css';
4879
+ 2. Wrap your app in <BakeThereProvider> from '${componentsAlias}/BakeThereProvider'
4880
+ 3. Run 'bakethere add <component>' to add components`);
4881
+ });
4882
+
4883
+ // src/commands/add.ts
4884
+ var import_commander3 = require("commander");
4885
+ var import_prompts3 = require("@clack/prompts");
4886
+ var import_path5 = require("path");
4887
+ var import_fs5 = require("fs");
4888
+
4889
+ // src/lib/rewrite.ts
4890
+ function rewriteImports(source, config) {
4891
+ const { componentsAlias, aliases } = config;
4892
+ let result = source.replace(
4893
+ /from\s+"\.\.\/\.\.\/provider"|from\s+'\.\.\/\.\.\/provider'/g,
4894
+ `from "${componentsAlias}/BakeThereProvider"`
4895
+ );
4896
+ result = result.replace(
4897
+ /from\s+"\.\.\/provider"|from\s+'\.\.\/provider'/g,
4898
+ `from "${componentsAlias}/BakeThereProvider"`
4899
+ );
4900
+ result = result.replace(
4901
+ /from\s+"\.\.\/\.\.\/types"|from\s+'\.\.\/\.\.\/types'/g,
4902
+ `from "${componentsAlias}/BakeThereProvider"`
4903
+ );
4904
+ result = result.replace(
4905
+ /from\s+"\.\.\/types"|from\s+'\.\.\/types'/g,
4906
+ `from "${componentsAlias}/BakeThereProvider"`
4907
+ );
4908
+ if (aliases.utils !== "@/lib/utils") {
4909
+ result = result.replace(
4910
+ /from\s+"@\/lib\/utils"|from\s+'@\/lib\/utils'/g,
4911
+ `from "${aliases.utils}"`
4912
+ );
4913
+ }
4914
+ result = result.replace(
4915
+ /from\s+["'](\.\.\/\.\.\/|\.\.\/)[^"']+["']/g,
4916
+ (match) => {
4917
+ const pathMatch = match.match(/["'](\.\.\/\.\.\/|\.\.\/)(.*?)["']/);
4918
+ if (!pathMatch) return match;
4919
+ const segments = pathMatch[2].split("/");
4920
+ const raw = segments[segments.length - 1];
4921
+ const pascalSegment = raw.charAt(0).toUpperCase() + raw.slice(1);
4922
+ return `from "${componentsAlias}/${pascalSegment}"`;
4923
+ }
4924
+ );
4925
+ return result;
4926
+ }
4927
+
4928
+ // src/commands/add.ts
4929
+ function resolveComponents(names) {
4930
+ const visited = /* @__PURE__ */ new Set();
4931
+ const infraDeps = /* @__PURE__ */ new Set();
4932
+ const queue = [...names];
4933
+ while (queue.length > 0) {
4934
+ const name = queue.shift();
4935
+ if (visited.has(name)) continue;
4936
+ visited.add(name);
4937
+ const comp = registry[name];
4938
+ if (!comp) continue;
4939
+ comp.infraDeps.forEach((d) => infraDeps.add(d));
4940
+ comp.componentDeps.forEach((d) => {
4941
+ if (!visited.has(d)) queue.push(d);
4942
+ });
4943
+ }
4944
+ return { components: Array.from(visited), infraDeps };
4945
+ }
4946
+ function rewriteFiles(files, config) {
4947
+ return files.map((f) => ({
4948
+ ...f,
4949
+ content: f.name.endsWith(".css") ? f.content : rewriteImports(f.content, config)
4950
+ }));
4951
+ }
4952
+ var addCommand = new import_commander3.Command("add").description("Add one or more BakeThere components to your project").argument("[components...]", "Component names to add").option("-o, --overwrite", "Overwrite existing files", false).option("--path <dir>", "Override output directory for component files").option("-y, --yes", "Skip prompts and use defaults", false).action(async (componentArgs, options) => {
4953
+ const cwd = process.cwd();
4954
+ (0, import_prompts3.intro)("BakeThere");
4955
+ const unknown = componentArgs.filter((n) => !registry[n]);
4956
+ if (unknown.length > 0) {
4957
+ for (const name of unknown) {
4958
+ import_prompts3.log.error(`Unknown component: "${name}". Run 'bakethere list' to see available components.`);
4959
+ }
4960
+ process.exit(1);
4961
+ }
4962
+ let selectedNames = componentArgs;
4963
+ if (selectedNames.length === 0 && options.yes) {
4964
+ import_prompts3.log.error("Specify component names when using --yes. Example: bakethere add button card");
4965
+ process.exit(1);
4966
+ }
4967
+ if (selectedNames.length === 0) {
4968
+ const allOptions = Object.entries(categories).flatMap(
4969
+ ([cat, names]) => names.map((n) => ({ value: n, label: n, hint: cat }))
4970
+ );
4971
+ const selected = await (0, import_prompts3.multiselect)({
4972
+ message: "Which components would you like to add?",
4973
+ options: allOptions,
4974
+ required: true
4975
+ });
4976
+ if ((0, import_prompts3.isCancel)(selected)) {
4977
+ import_prompts3.log.info("Cancelled.");
4978
+ process.exit(0);
4979
+ }
4980
+ selectedNames = selected;
4981
+ }
4982
+ if (!hasConfig(cwd)) {
4983
+ import_prompts3.log.warn("bakethere.json not found. Running init first...");
4984
+ const detected2 = detectProject(cwd);
4985
+ const config2 = {
4986
+ componentsDir: detected2.hasSrcDir ? "src/components/ui" : "components/ui",
4987
+ componentsAlias: "@/components/ui",
4988
+ stylesDir: detected2.hasSrcDir ? "src/styles" : "styles",
4989
+ aliases: { utils: "@/lib/utils" }
4990
+ };
4991
+ writeConfig(cwd, config2);
4992
+ import_prompts3.log.info("bakethere.json created with defaults. Edit it to customize paths.");
4993
+ }
4994
+ const config = readConfig(cwd);
4995
+ const componentsDir = options.path ? (0, import_path5.resolve)(cwd, options.path) : (0, import_path5.join)(cwd, config.componentsDir);
4996
+ const stylesDir = (0, import_path5.join)(cwd, config.stylesDir);
4997
+ const detected = detectProject(cwd);
4998
+ const utilsDir = (0, import_path5.resolve)(cwd, detected.hasSrcDir ? "src/lib" : "lib");
4999
+ const { components, infraDeps } = resolveComponents(selectedNames);
5000
+ const s = (0, import_prompts3.spinner)();
5001
+ s.start("Setting up infrastructure");
5002
+ (0, import_fs5.mkdirSync)(stylesDir, { recursive: true });
5003
+ (0, import_fs5.mkdirSync)(componentsDir, { recursive: true });
5004
+ (0, import_fs5.mkdirSync)(utilsDir, { recursive: true });
5005
+ const stylesInfra = [];
5006
+ const utilsInfra = [];
5007
+ const componentInfra = [];
5008
+ for (const dep of infraDeps) {
5009
+ const file = infra[dep];
5010
+ if (!file) continue;
5011
+ if (file.name.endsWith(".css")) {
5012
+ stylesInfra.push(file);
5013
+ } else if (dep === "utils") {
5014
+ utilsInfra.push(file);
5015
+ } else {
5016
+ componentInfra.push(file);
5017
+ }
5018
+ }
5019
+ copyFiles(stylesInfra, stylesDir, { overwrite: options.overwrite });
5020
+ copyFiles(utilsInfra, utilsDir, { overwrite: options.overwrite });
5021
+ copyFiles(componentInfra, componentsDir, { overwrite: options.overwrite });
5022
+ s.stop("Infrastructure ready");
5023
+ const npmDepsNeeded = [];
5024
+ let hasErrors = false;
5025
+ for (const name of components) {
5026
+ const comp = registry[name];
5027
+ const rewritten = rewriteFiles(comp.files, config);
5028
+ const results = copyFiles(rewritten, componentsDir, { overwrite: options.overwrite });
5029
+ for (const r of results) {
5030
+ const rel = (0, import_path5.relative)(cwd, r.path);
5031
+ if (r.error) {
5032
+ import_prompts3.log.error(`Failed to write ${rel}: ${r.error}`);
5033
+ hasErrors = true;
5034
+ } else if (r.skipped) {
5035
+ import_prompts3.log.warn(`Skipped (already exists): ${rel}`);
5036
+ } else {
5037
+ import_prompts3.log.success(`Added: ${rel}`);
5038
+ }
5039
+ }
5040
+ comp.npmDeps.forEach((d) => npmDepsNeeded.push(d));
5041
+ }
5042
+ if (npmDepsNeeded.length > 0) {
5043
+ import_prompts3.log.info(`Install peer dependencies:
5044
+ npm install ${[...new Set(npmDepsNeeded)].join(" ")}`);
5045
+ }
5046
+ if (hasErrors) {
5047
+ (0, import_prompts3.outro)("Done with errors. Some files may not have been written.");
5048
+ process.exit(1);
5049
+ } else {
5050
+ (0, import_prompts3.outro)(`Done! ${components.length} component(s) added.`);
5051
+ }
5052
+ });
5053
+
5054
+ // src/cli.ts
5055
+ import_commander4.program.name("bakethere").description("Add BakeThere components to your project").version("0.1.0");
5056
+ import_commander4.program.addCommand(listCommand);
5057
+ import_commander4.program.addCommand(initCommand);
5058
+ import_commander4.program.addCommand(addCommand);
5059
+ import_commander4.program.parse();