{
  "$schema": "https://ui.shadcn.com/schema/registry.json",
  "name": "create-stack",
  "homepage": "https://create-stack.alfredmouelle.com",
  "items": [
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "date-picker",
      "title": "Date Picker",
      "description": "Single-date and date-range pickers with a calendar popover.",
      "dependencies": [
        "react-day-picker",
        "date-fns",
        "lucide-react"
      ],
      "registryDependencies": [
        "calendar",
        "popover",
        "button"
      ],
      "files": [
        {
          "path": "ui/date-picker.tsx",
          "content": "'use client'\n\nimport { format, isValid, parseISO } from 'date-fns'\nimport { CalendarIcon, X } from 'lucide-react'\nimport { useState } from 'react'\nimport { Button } from '@/components/ui/button'\nimport { Calendar } from '@/components/ui/calendar'\nimport { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'\nimport { toISODate } from '@/lib/date'\nimport { cn } from '@/lib/utils'\n\ninterface DatePickerProps {\n  value: string\n  onChange: (value: string) => void\n  placeholder?: string\n  disabled?: boolean\n  className?: string\n}\n\nexport function DatePicker({\n  value,\n  onChange,\n  placeholder = 'Pick a date',\n  disabled,\n  className,\n}: DatePickerProps) {\n  const [open, setOpen] = useState(false)\n  const parsed = value ? parseISO(value) : undefined\n  const date = parsed && isValid(parsed) ? parsed : undefined\n\n  const clear = () => {\n    onChange('')\n    setOpen(false)\n  }\n\n  return (\n    <Popover onOpenChange={setOpen} open={open}>\n      <div className={cn('group relative w-full', className)}>\n        <PopoverTrigger asChild>\n          <Button\n            className={cn(\n              'w-full cursor-pointer justify-start px-3 text-left font-normal',\n              !date && 'text-muted-foreground',\n            )}\n            disabled={disabled}\n            variant=\"outline\"\n          >\n            <CalendarIcon\n              className={cn(\n                'mr-2 size-4 opacity-50 transition-opacity',\n                date && !disabled && 'group-hover:opacity-0',\n              )}\n            />\n            {date ? format(date, 'PPP') : <span>{placeholder}</span>}\n          </Button>\n        </PopoverTrigger>\n\n        {date && !disabled && (\n          <button\n            aria-label=\"Clear date\"\n            className=\"absolute top-1/2 left-3 z-10 flex size-4 -translate-y-1/2 cursor-pointer items-center justify-center text-muted-foreground opacity-0 transition-opacity hover:text-destructive focus-visible:opacity-100 group-hover:opacity-100\"\n            onClick={clear}\n            type=\"button\"\n          >\n            <X className=\"size-4\" />\n          </button>\n        )}\n      </div>\n\n      <PopoverContent align=\"start\" className=\"w-auto p-0\">\n        <Calendar\n          autoFocus\n          captionLayout=\"dropdown\"\n          defaultMonth={date}\n          endMonth={new Date(new Date().getFullYear() + 1, 11)}\n          mode=\"single\"\n          onSelect={(selected) => {\n            if (selected) {\n              onChange(toISODate(selected))\n              setOpen(false)\n            }\n          }}\n          selected={date}\n          startMonth={new Date(2015, 0)}\n        />\n      </PopoverContent>\n    </Popover>\n  )\n}\n",
          "type": "registry:ui"
        },
        {
          "path": "ui/date-range-picker.tsx",
          "content": "'use client'\n\nimport { format, parseISO } from 'date-fns'\nimport { CalendarRange, X } from 'lucide-react'\nimport { type ComponentProps, useState } from 'react'\nimport type { DateRange } from 'react-day-picker'\nimport { Button } from '@/components/ui/button'\nimport { Calendar } from '@/components/ui/calendar'\nimport { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'\nimport { toISODate } from '@/lib/date'\nimport { cn } from '@/lib/utils'\n\nexport interface DateRangeValue {\n  from: string\n  to: string\n}\n\ninterface DateRangePickerProps {\n  value: DateRangeValue | null\n  onChange: (value: DateRangeValue | null) => void\n  placeholder?: string\n  numberOfMonths?: number\n  align?: ComponentProps<typeof PopoverContent>['align']\n  triggerVariant?: ComponentProps<typeof Button>['variant']\n  triggerClassName?: string\n  formatLabel?: (value: DateRangeValue) => string\n}\n\nconst defaultFormatLabel = (value: DateRangeValue) => {\n  const from = parseISO(value.from)\n  const to = parseISO(value.to)\n  const pattern = from.getFullYear() === to.getFullYear() ? 'd MMM' : 'd MMM yyyy'\n  return `${format(from, pattern)} – ${format(to, pattern)}`\n}\n\nexport function DateRangePicker({\n  value,\n  onChange,\n  placeholder = 'Pick a range',\n  numberOfMonths = 2,\n  align = 'end',\n  triggerVariant = 'outline',\n  triggerClassName,\n  formatLabel = defaultFormatLabel,\n}: DateRangePickerProps) {\n  const [open, setOpen] = useState(false)\n  const [draft, setDraft] = useState<DateRange | undefined>()\n  const hasValue = value !== null\n\n  const handleOpenChange = (next: boolean) => {\n    if (next) {\n      setDraft(value ? { from: parseISO(value.from), to: parseISO(value.to) } : undefined)\n    }\n    setOpen(next)\n  }\n\n  const applyDraft = () => {\n    if (!(draft?.from && draft.to)) return\n    onChange({\n      from: toISODate(draft.from),\n      to: toISODate(draft.to),\n    })\n    setOpen(false)\n  }\n\n  const reset = () => {\n    setDraft(undefined)\n    onChange(null)\n    setOpen(false)\n  }\n\n  return (\n    <Popover onOpenChange={handleOpenChange} open={open}>\n      <div className=\"group relative inline-flex shrink-0\">\n        <PopoverTrigger asChild>\n          <Button\n            className={cn('cursor-pointer', triggerClassName)}\n            size=\"sm\"\n            variant={hasValue ? 'secondary' : triggerVariant}\n          >\n            <CalendarRange\n              className={cn('size-4 transition-opacity', hasValue && 'group-hover:opacity-0')}\n            />\n            {hasValue ? formatLabel(value) : placeholder}\n          </Button>\n        </PopoverTrigger>\n\n        {hasValue && (\n          <button\n            aria-label=\"Clear range\"\n            className=\"absolute top-1/2 left-3 z-10 flex size-4 -translate-y-1/2 cursor-pointer items-center justify-center text-muted-foreground opacity-0 transition-opacity hover:text-destructive focus-visible:opacity-100 group-hover:opacity-100\"\n            onClick={(event) => {\n              event.stopPropagation()\n              reset()\n            }}\n            type=\"button\"\n          >\n            <X className=\"size-4\" />\n          </button>\n        )}\n      </div>\n      <PopoverContent align={align} className=\"w-auto p-0\">\n        <Calendar\n          autoFocus\n          captionLayout=\"dropdown\"\n          defaultMonth={draft?.from}\n          endMonth={new Date(new Date().getFullYear() + 1, 11)}\n          mode=\"range\"\n          numberOfMonths={numberOfMonths}\n          onSelect={setDraft}\n          selected={draft}\n          startMonth={new Date(2015, 0)}\n        />\n        <div className=\"flex items-center justify-between gap-2 border-t p-2\">\n          <Button\n            className=\"cursor-pointer\"\n            disabled={!(hasValue || draft?.from)}\n            onClick={reset}\n            size=\"sm\"\n            variant=\"ghost\"\n          >\n            Reset\n          </Button>\n          <div className=\"flex gap-2\">\n            <Button\n              className=\"cursor-pointer\"\n              onClick={() => setOpen(false)}\n              size=\"sm\"\n              variant=\"ghost\"\n            >\n              Cancel\n            </Button>\n            <Button\n              className=\"cursor-pointer\"\n              disabled={!(draft?.from && draft.to)}\n              onClick={applyDraft}\n              size=\"sm\"\n            >\n              Apply\n            </Button>\n          </div>\n        </div>\n      </PopoverContent>\n    </Popover>\n  )\n}\n",
          "type": "registry:ui"
        },
        {
          "path": "lib/date.ts",
          "content": "import { format } from 'date-fns'\n\nexport const toISODate = (date: Date): string => format(date, 'yyyy-MM-dd')\n",
          "type": "registry:lib"
        }
      ],
      "type": "registry:component"
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "prompt",
      "title": "Prompt",
      "description": "A callable dialog that waits for text input.",
      "dependencies": [
        "react-call"
      ],
      "registryDependencies": [
        "dialog",
        "button",
        "input",
        "label"
      ],
      "files": [
        {
          "path": "ui/prompt.tsx",
          "content": "'use client'\n\n\nimport { useId, useState } from 'react'\nimport { createCallable } from 'react-call'\n\nimport { Button } from '@/components/ui/button'\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n} from '@/components/ui/dialog'\nimport { Input } from '@/components/ui/input'\nimport { Label } from '@/components/ui/label'\n\nexport interface PromptProps {\n  title: string\n  description?: string\n  label?: string\n  defaultValue?: string\n  placeholder?: string\n  confirmLabel?: string\n  cancelLabel?: string\n  required?: boolean\n}\n\nexport const Prompt = createCallable<PromptProps, string | null>(\n  ({\n    call,\n    title,\n    description,\n    label,\n    defaultValue = '',\n    placeholder,\n    confirmLabel = 'Confirm',\n    cancelLabel = 'Cancel',\n    required = true,\n  }) => {\n    const id = useId()\n    const [value, setValue] = useState(defaultValue)\n    const canSubmit = !required || value.trim().length > 0\n\n    return (\n      <Dialog open={!call.ended} onOpenChange={(open) => !open && call.end(null)}>\n        <DialogContent showCloseButton={false}>\n          <form\n            className=\"grid gap-6\"\n            onSubmit={(event) => {\n              event.preventDefault()\n              if (canSubmit) call.end(value)\n            }}\n          >\n            <DialogHeader>\n              <DialogTitle>{title}</DialogTitle>\n              {description && <DialogDescription>{description}</DialogDescription>}\n            </DialogHeader>\n            <div className=\"grid gap-2\">\n              {label && <Label htmlFor={id}>{label}</Label>}\n              <Input\n                id={id}\n                // biome-ignore lint/a11y/noAutofocus: focus the field the dialog exists for\n                autoFocus\n                placeholder={placeholder}\n                value={value}\n                onChange={(event) => setValue(event.target.value)}\n              />\n            </div>\n            <DialogFooter>\n              <Button type=\"button\" variant=\"outline\" onClick={() => call.end(null)}>\n                {cancelLabel}\n              </Button>\n              <Button type=\"submit\" disabled={!canSubmit}>\n                {confirmLabel}\n              </Button>\n            </DialogFooter>\n          </form>\n        </DialogContent>\n      </Dialog>\n    )\n  },\n  200,\n)\n",
          "type": "registry:ui"
        }
      ],
      "type": "registry:component"
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "choice",
      "title": "Choice",
      "description": "A callable dialog that waits for a selection.",
      "dependencies": [
        "react-call"
      ],
      "registryDependencies": [
        "dialog",
        "button"
      ],
      "files": [
        {
          "path": "ui/choice.tsx",
          "content": "'use client'\n\n\nimport { createCallable } from 'react-call'\n\nimport { Button } from '@/components/ui/button'\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogHeader,\n  DialogTitle,\n} from '@/components/ui/dialog'\n\nexport interface ChoiceOption {\n  label: string\n  value: string\n  description?: string\n}\n\nexport interface ChoiceProps {\n  title: string\n  description?: string\n  options: ChoiceOption[]\n}\n\nexport const Choice = createCallable<ChoiceProps, string | null>(\n  ({ call, title, description, options }) => (\n    <Dialog open={!call.ended} onOpenChange={(open) => !open && call.end(null)}>\n      <DialogContent>\n        <DialogHeader>\n          <DialogTitle>{title}</DialogTitle>\n          {description && <DialogDescription>{description}</DialogDescription>}\n        </DialogHeader>\n        <div className=\"grid gap-2\">\n          {options.map((option) => (\n            <Button\n              key={option.value}\n              variant=\"outline\"\n              className=\"h-auto flex-col items-start gap-0.5 whitespace-normal py-2.5 text-left\"\n              onClick={() => call.end(option.value)}\n            >\n              <span className=\"font-medium\">{option.label}</span>\n              {option.description && (\n                <span className=\"text-muted-foreground text-xs\">{option.description}</span>\n              )}\n            </Button>\n          ))}\n        </div>\n      </DialogContent>\n    </Dialog>\n  ),\n  200,\n)\n",
          "type": "registry:ui"
        }
      ],
      "type": "registry:component"
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "confirm-passphrase",
      "title": "Confirm Passphrase",
      "description": "A callable dialog that checks an exact phrase.",
      "dependencies": [
        "react-call"
      ],
      "registryDependencies": [
        "dialog",
        "button",
        "input",
        "label"
      ],
      "files": [
        {
          "path": "ui/confirm-passphrase.tsx",
          "content": "'use client'\n\n\nimport { useId, useState } from 'react'\nimport { createCallable } from 'react-call'\n\nimport { Button } from '@/components/ui/button'\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n} from '@/components/ui/dialog'\nimport { Input } from '@/components/ui/input'\nimport { Label } from '@/components/ui/label'\n\nexport interface ConfirmPassphraseProps {\n  title: string\n  description?: string\n  phrase: string\n  confirmLabel?: string\n  cancelLabel?: string\n  variant?: 'default' | 'destructive'\n}\n\nexport const ConfirmPassphrase = createCallable<ConfirmPassphraseProps, boolean>(\n  ({\n    call,\n    title,\n    description,\n    phrase,\n    confirmLabel = 'Confirm',\n    cancelLabel = 'Cancel',\n    variant = 'destructive',\n  }) => {\n    const id = useId()\n    const [value, setValue] = useState('')\n    const matches = value === phrase\n\n    return (\n      <Dialog open={!call.ended} onOpenChange={(open) => !open && call.end(false)}>\n        <DialogContent showCloseButton={false}>\n          <form\n            className=\"grid gap-6\"\n            onSubmit={(event) => {\n              event.preventDefault()\n              if (matches) call.end(true)\n            }}\n          >\n            <DialogHeader>\n              <DialogTitle>{title}</DialogTitle>\n              {description && <DialogDescription>{description}</DialogDescription>}\n            </DialogHeader>\n            <div className=\"grid gap-2\">\n              <Label htmlFor={id}>\n                Type <span className=\"font-medium text-foreground\">{phrase}</span> to continue\n              </Label>\n              <Input\n                id={id}\n                autoComplete=\"off\"\n                // biome-ignore lint/a11y/noAutofocus: focus the field the dialog exists for\n                autoFocus\n                value={value}\n                onChange={(event) => setValue(event.target.value)}\n              />\n            </div>\n            <DialogFooter>\n              <Button type=\"button\" variant=\"outline\" onClick={() => call.end(false)}>\n                {cancelLabel}\n              </Button>\n              <Button type=\"submit\" variant={variant} disabled={!matches}>\n                {confirmLabel}\n              </Button>\n            </DialogFooter>\n          </form>\n        </DialogContent>\n      </Dialog>\n    )\n  },\n  200,\n)\n",
          "type": "registry:ui"
        }
      ],
      "type": "registry:component"
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "confirm-otp",
      "title": "Confirm OTP",
      "description": "A callable dialog that checks a one-time password.",
      "dependencies": [
        "react-call"
      ],
      "registryDependencies": [
        "dialog",
        "button",
        "input-otp"
      ],
      "files": [
        {
          "path": "ui/confirm-otp.tsx",
          "content": "'use client'\n\n\nimport { useState } from 'react'\nimport { createCallable } from 'react-call'\n\nimport { Button } from '@/components/ui/button'\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n} from '@/components/ui/dialog'\nimport { InputOTP, InputOTPGroup, InputOTPSlot } from '@/components/ui/input-otp'\n\nexport interface ConfirmOtpProps {\n  title: string\n  description?: string\n  length?: number\n  confirmLabel?: string\n  cancelLabel?: string\n  verify: (code: string) => Promise<boolean>\n}\n\nexport const ConfirmOtp = createCallable<ConfirmOtpProps, boolean>(\n  ({\n    call,\n    title,\n    description,\n    length = 6,\n    confirmLabel = 'Verify',\n    cancelLabel = 'Cancel',\n    verify,\n  }) => {\n    const [value, setValue] = useState('')\n    const [error, setError] = useState<string | null>(null)\n    const [pending, setPending] = useState(false)\n\n    const submit = async (code: string) => {\n      setPending(true)\n      setError(null)\n      try {\n        if (await verify(code)) {\n          call.end(true)\n          return\n        }\n        setError('Invalid code. Try again.')\n      } catch {\n        setError('Something went wrong. Try again.')\n      }\n      setValue('')\n      setPending(false)\n    }\n\n    return (\n      <Dialog open={!call.ended} onOpenChange={(open) => !open && call.end(false)}>\n        <DialogContent showCloseButton={false}>\n          <form\n            className=\"grid gap-6\"\n            onSubmit={(event) => {\n              event.preventDefault()\n              if (value.length === length && !pending) submit(value)\n            }}\n          >\n            <DialogHeader>\n              <DialogTitle>{title}</DialogTitle>\n              {description && <DialogDescription>{description}</DialogDescription>}\n            </DialogHeader>\n            <div className=\"grid justify-items-center gap-2\">\n              <InputOTP\n                maxLength={length}\n                value={value}\n                disabled={pending}\n                onChange={setValue}\n                onComplete={submit}\n                // biome-ignore lint/a11y/noAutofocus: focus the code input the dialog exists for\n                autoFocus\n              >\n                <InputOTPGroup>\n                  {Array.from({ length }, (_, index) => (\n                    <InputOTPSlot key={index} index={index} />\n                  ))}\n                </InputOTPGroup>\n              </InputOTP>\n              <p className=\"min-h-5 text-destructive text-sm\" role=\"alert\">\n                {error}\n              </p>\n            </div>\n            <DialogFooter>\n              <Button\n                type=\"button\"\n                variant=\"outline\"\n                disabled={pending}\n                onClick={() => call.end(false)}\n              >\n                {cancelLabel}\n              </Button>\n              <Button type=\"submit\" disabled={value.length !== length || pending}>\n                {pending ? 'Verifying...' : confirmLabel}\n              </Button>\n            </DialogFooter>\n          </form>\n        </DialogContent>\n      </Dialog>\n    )\n  },\n  200,\n)\n",
          "type": "registry:ui"
        }
      ],
      "type": "registry:component"
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "confirm",
      "title": "Confirm",
      "description": "Awaitable yes/no confirmation built on react-call and alert-dialog.",
      "dependencies": [
        "react-call"
      ],
      "registryDependencies": [
        "alert-dialog"
      ],
      "files": [
        {
          "path": "ui/confirm.tsx",
          "content": "'use client'\n\n\nimport { createCallable } from 'react-call'\n\nimport {\n  AlertDialog,\n  AlertDialogAction,\n  AlertDialogCancel,\n  AlertDialogContent,\n  AlertDialogDescription,\n  AlertDialogFooter,\n  AlertDialogHeader,\n  AlertDialogTitle,\n} from '@/components/ui/alert-dialog'\n\nexport interface ConfirmProps {\n  title: string\n  description?: string\n  confirmLabel?: string\n  cancelLabel?: string\n  variant?: 'default' | 'destructive'\n}\n\nexport const Confirm = createCallable<ConfirmProps, boolean>(\n  ({\n    call,\n    title,\n    description,\n    confirmLabel = 'Confirm',\n    cancelLabel = 'Cancel',\n    variant = 'default',\n  }) => (\n    <AlertDialog open={!call.ended} onOpenChange={(open) => !open && call.end(false)}>\n      <AlertDialogContent>\n        <AlertDialogHeader>\n          <AlertDialogTitle>{title}</AlertDialogTitle>\n          {description && <AlertDialogDescription>{description}</AlertDialogDescription>}\n        </AlertDialogHeader>\n        <AlertDialogFooter>\n          <AlertDialogCancel onClick={() => call.end(false)}>{cancelLabel}</AlertDialogCancel>\n          <AlertDialogAction variant={variant} onClick={() => call.end(true)}>\n            {confirmLabel}\n          </AlertDialogAction>\n        </AlertDialogFooter>\n      </AlertDialogContent>\n    </AlertDialog>\n  ),\n  200,\n)\n",
          "type": "registry:ui"
        }
      ],
      "type": "registry:component"
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "alert",
      "title": "Alert",
      "description": "Awaitable acknowledgement built on react-call and alert-dialog.",
      "dependencies": [
        "react-call"
      ],
      "registryDependencies": [
        "alert-dialog"
      ],
      "files": [
        {
          "path": "ui/alert.tsx",
          "content": "'use client'\n\n\nimport { createCallable } from 'react-call'\n\nimport {\n  AlertDialog,\n  AlertDialogAction,\n  AlertDialogContent,\n  AlertDialogDescription,\n  AlertDialogFooter,\n  AlertDialogHeader,\n  AlertDialogTitle,\n} from '@/components/ui/alert-dialog'\n\nexport interface AlertProps {\n  title: string\n  description?: string\n  confirmLabel?: string\n}\n\nexport const Alert = createCallable<AlertProps, void>(\n  ({ call, title, description, confirmLabel = 'OK' }) => (\n    <AlertDialog open={!call.ended} onOpenChange={(open) => !open && call.end()}>\n      <AlertDialogContent>\n        <AlertDialogHeader>\n          <AlertDialogTitle>{title}</AlertDialogTitle>\n          {description && <AlertDialogDescription>{description}</AlertDialogDescription>}\n        </AlertDialogHeader>\n        <AlertDialogFooter>\n          <AlertDialogAction onClick={() => call.end()}>{confirmLabel}</AlertDialogAction>\n        </AlertDialogFooter>\n      </AlertDialogContent>\n    </AlertDialog>\n  ),\n  200,\n)\n",
          "type": "registry:ui"
        }
      ],
      "type": "registry:component"
    },
    {
      "$schema": "https://ui.shadcn.com/schema/registry-item.json",
      "name": "data-table",
      "title": "Data Table",
      "description": "Standard and infinite TanStack tables with sorting and visibility state.",
      "dependencies": [
        "@tanstack/react-table@^8.21.3",
        "lucide-react"
      ],
      "registryDependencies": [
        "table",
        "skeleton",
        "button"
      ],
      "files": [
        {
          "path": "components/data-table.tsx",
          "content": "import { flexRender, type Table as ReactTable } from '@tanstack/react-table'\nimport { Skeleton } from '@/components/ui/skeleton'\nimport {\n  Table,\n  TableBody,\n  TableCell,\n  TableHead,\n  TableHeader,\n  TableRow,\n} from '@/components/ui/table'\n\ninterface DataTableProps<TData> {\n  table: ReactTable<TData>\n  columnCount: number\n  isLoading: boolean\n  emptyLabel: string\n  skeletonRows?: number\n}\n\nexport function DataTable<TData>({\n  table,\n  columnCount,\n  isLoading,\n  emptyLabel,\n  skeletonRows = 3,\n}: DataTableProps<TData>) {\n  const rows = table.getRowModel().rows\n\n  return (\n    <div className=\"overflow-hidden rounded-lg border\">\n      <Table>\n        <TableHeader>\n          {table.getHeaderGroups().map((headerGroup) => (\n            <TableRow key={headerGroup.id}>\n              {headerGroup.headers.map((header) => (\n                <TableHead key={header.id}>\n                  {header.isPlaceholder\n                    ? null\n                    : flexRender(header.column.columnDef.header, header.getContext())}\n                </TableHead>\n              ))}\n            </TableRow>\n          ))}\n        </TableHeader>\n        <TableBody>\n          {isLoading ? (\n            Array.from({ length: skeletonRows }, (_, rowIndex) => (\n              <TableRow key={rowIndex}>\n                {Array.from({ length: columnCount }, (_, cellIndex) => (\n                  <TableCell key={cellIndex}>\n                    <Skeleton className=\"h-6 w-full\" />\n                  </TableCell>\n                ))}\n              </TableRow>\n            ))\n          ) : rows.length === 0 ? (\n            <TableRow>\n              <TableCell className=\"h-24 text-center text-muted-foreground\" colSpan={columnCount}>\n                {emptyLabel}\n              </TableCell>\n            </TableRow>\n          ) : (\n            rows.map((row) => (\n              <TableRow className=\"group\" key={row.id}>\n                {row.getVisibleCells().map((cell) => (\n                  <TableCell key={cell.id}>\n                    {flexRender(cell.column.columnDef.cell, cell.getContext())}\n                  </TableCell>\n                ))}\n              </TableRow>\n            ))\n          )}\n        </TableBody>\n      </Table>\n    </div>\n  )\n}\n",
          "type": "registry:component"
        },
        {
          "path": "components/infinite-data-table.tsx",
          "content": "import { flexRender, type Table as ReactTable } from '@tanstack/react-table'\nimport { Skeleton } from '@/components/ui/skeleton'\nimport {\n  Table,\n  TableBody,\n  TableCell,\n  TableHead,\n  TableHeader,\n  TableRow,\n} from '@/components/ui/table'\nimport { cn } from '@/lib/utils'\n\ninterface InfiniteDataTableProps<TData> {\n  table: ReactTable<TData>\n  columnCount: number\n  isLoading: boolean\n  isRefetching: boolean\n  isFetchingNextPage: boolean\n  hasFilters: boolean\n  emptyLabel: string\n  emptyFilteredLabel: string\n  sentinelRef: (node: HTMLDivElement | null) => void\n}\n\nexport function InfiniteDataTable<TData>({\n  table,\n  columnCount,\n  isLoading,\n  isRefetching,\n  isFetchingNextPage,\n  hasFilters,\n  emptyLabel,\n  emptyFilteredLabel,\n  sentinelRef,\n}: InfiniteDataTableProps<TData>) {\n  const dataRows = table.getRowModel().rows\n\n  return (\n    <div\n      className={cn(\n        'overflow-hidden rounded-lg border transition-opacity',\n        isRefetching && 'opacity-60',\n      )}\n    >\n      <Table>\n        <TableHeader>\n          {table.getHeaderGroups().map((headerGroup) => (\n            <TableRow key={headerGroup.id}>\n              {headerGroup.headers.map((header) => (\n                <TableHead key={header.id}>\n                  {header.isPlaceholder\n                    ? null\n                    : flexRender(header.column.columnDef.header, header.getContext())}\n                </TableHead>\n              ))}\n            </TableRow>\n          ))}\n        </TableHeader>\n        <TableBody>\n          {isLoading ? <SkeletonRows columns={columnCount} /> : null}\n\n          {!isLoading && dataRows.length === 0 ? (\n            <TableRow>\n              <TableCell className=\"h-24 text-center text-muted-foreground\" colSpan={columnCount}>\n                {hasFilters ? emptyFilteredLabel : emptyLabel}\n              </TableCell>\n            </TableRow>\n          ) : null}\n\n          {dataRows.map((row) => (\n            <TableRow className=\"group\" key={row.id}>\n              {row.getVisibleCells().map((cell) => (\n                <TableCell key={cell.id}>\n                  {flexRender(cell.column.columnDef.cell, cell.getContext())}\n                </TableCell>\n              ))}\n            </TableRow>\n          ))}\n\n          {isFetchingNextPage ? <SkeletonRows columns={columnCount} /> : null}\n        </TableBody>\n      </Table>\n      <div ref={sentinelRef} />\n    </div>\n  )\n}\n\nfunction SkeletonRows({ columns }: { columns: number }) {\n  return (\n    <>\n      {Array.from({ length: 5 }, (_, rowIndex) => (\n        <TableRow key={rowIndex}>\n          {Array.from({ length: columns }, (_, cellIndex) => (\n            <TableCell key={cellIndex}>\n              <Skeleton className=\"h-6 w-full\" />\n            </TableCell>\n          ))}\n        </TableRow>\n      ))}\n    </>\n  )\n}\n",
          "type": "registry:component"
        },
        {
          "path": "components/sortable-header.tsx",
          "content": "import { ArrowDown, ArrowUp, ArrowUpDown } from 'lucide-react'\nimport { Button } from '@/components/ui/button'\n\nexport interface SortState<Field extends string> {\n  field: Field\n  direction: 'asc' | 'desc'\n}\n\ninterface SortableHeaderProps<Field extends string> {\n  label: string\n  field: Field\n  sort: SortState<Field>\n  onSort: (field: Field) => void\n}\n\nexport function SortableHeader<Field extends string>({\n  label,\n  field,\n  sort,\n  onSort,\n}: SortableHeaderProps<Field>) {\n  const active = sort.field === field\n  const Icon = active ? (sort.direction === 'asc' ? ArrowUp : ArrowDown) : ArrowUpDown\n\n  return (\n    <Button\n      className=\"-ml-2 h-8 cursor-pointer text-muted-foreground data-[active=true]:text-foreground\"\n      data-active={active}\n      onClick={() => onSort(field)}\n      size=\"sm\"\n      variant=\"ghost\"\n    >\n      {label}\n      <Icon className=\"size-3.5\" />\n    </Button>\n  )\n}\n",
          "type": "registry:component"
        },
        {
          "path": "hooks/use-data-table.tsx",
          "content": "'use client'\n\nimport {\n  type ColumnDef,\n  type ColumnFiltersState,\n  getCoreRowModel,\n  getFilteredRowModel,\n  getSortedRowModel,\n  type SortingState,\n  type TableOptions,\n  useReactTable,\n  type VisibilityState,\n} from '@tanstack/react-table'\nimport { useEffect, useState } from 'react'\n\ninterface UseDataTableProps<TData> {\n  data: TData[]\n  // biome-ignore lint/suspicious/noExplicitAny: tanstack ColumnDef value type varies per column\n  columns: ColumnDef<TData, any>[]\n  storage?: { key: string; defaultVisibility?: VisibilityState }\n  options?: Omit<Partial<TableOptions<TData>>, 'data' | 'columns'>\n}\n\nconst storageKey = (key: string) => `data-table-visibility-${key}`\n\nfunction readVisibility(key: string): VisibilityState | null {\n  try {\n    const raw = localStorage.getItem(storageKey(key))\n    if (!raw) return null\n    const parsed: unknown = JSON.parse(raw)\n    return parsed && typeof parsed === 'object' ? (parsed as VisibilityState) : null\n  } catch {\n    return null\n  }\n}\n\nexport function useDataTable<TData>({ data, columns, storage, options }: UseDataTableProps<TData>) {\n  const [sorting, setSorting] = useState<SortingState>([])\n  const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])\n  const [columnVisibility, setColumnVisibility] = useState<VisibilityState>(\n    storage?.defaultVisibility ?? {},\n  )\n\n  const [isClient, setIsClient] = useState(false)\n  useEffect(() => {\n    setIsClient(true)\n  }, [])\n\n  useEffect(() => {\n    if (!isClient || !storage?.key) return\n    const stored = readVisibility(storage.key)\n    if (stored) setColumnVisibility((prev) => ({ ...prev, ...stored }))\n  }, [isClient, storage?.key])\n\n  useEffect(() => {\n    if (!isClient || !storage?.key) return\n    try {\n      localStorage.setItem(storageKey(storage.key), JSON.stringify(columnVisibility))\n    } catch {}\n  }, [columnVisibility, storage?.key, isClient])\n\n  const table = useReactTable({\n    ...options,\n    data,\n    columns,\n    state: { ...options?.state, sorting, columnFilters, columnVisibility },\n    getCoreRowModel: options?.getCoreRowModel ?? getCoreRowModel(),\n    getSortedRowModel: getSortedRowModel(),\n    getFilteredRowModel: getFilteredRowModel(),\n    onSortingChange: setSorting,\n    onColumnFiltersChange: setColumnFilters,\n    onColumnVisibilityChange: setColumnVisibility,\n  })\n\n  return {\n    table,\n    sorting,\n    setSorting,\n    columnFilters,\n    setColumnFilters,\n    columnVisibility,\n    setColumnVisibility,\n  }\n}\n",
          "type": "registry:hook"
        }
      ],
      "type": "registry:component"
    }
  ]
}
