{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"dashboard","title":"Dashboard","description":"Sidebar shell with stat cards, an interactive area chart and a sortable, selectable data table.","dependencies":["@tanstack/react-table@8.21.3","lucide-react@1.47.0","recharts@3.10.1"],"registryDependencies":["https://ward.so/r/badge.json","https://ward.so/r/button.json","https://ward.so/r/card.json","https://ward.so/r/chart.json","https://ward.so/r/checkbox.json","https://ward.so/r/dropdown-menu.json","https://ward.so/r/input.json","https://ward.so/r/select.json","https://ward.so/r/separator.json","https://ward.so/r/sidebar.json","https://ward.so/r/table.json","https://ward.so/r/tabs.json"],"files":[{"path":"registry/ward/blocks/dashboard/dashboard.tsx","content":"\"use client\"\n\n/*\n * Adapted from shadcn/ui (https://github.com/shadcn-ui/ui).\n *\n * MIT License\n *\n * Copyright (c) 2023 shadcn\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\nimport * as React from \"react\"\n\nimport { SidebarInset, SidebarProvider } from \"@/registry/ward/ui/sidebar\"\nimport { AppSidebar } from \"./app-sidebar\"\nimport { ChartArea } from \"./chart-area\"\nimport { DataTable } from \"./data-table\"\nimport { cards, chartConfig, chartData, columns, rows } from \"./data\"\nimport { SectionCards } from \"./section-cards\"\nimport { SiteHeader } from \"./site-header\"\n\nexport default function Dashboard() {\n  return (\n    <SidebarProvider style={{ \"--sidebar-width\": \"15rem\" } as React.CSSProperties}>\n      <AppSidebar variant=\"inset\" />\n      <SidebarInset>\n        <SiteHeader title=\"Overview\" />\n        <div className=\"flex flex-col gap-4 p-4 md:gap-6 md:p-6\">\n          <SectionCards cards={cards} />\n          <ChartArea data={chartData} config={chartConfig} title=\"Clicks by kind\" />\n          <DataTable columns={columns} data={rows} searchKey=\"title\" />\n        </div>\n      </SidebarInset>\n    </SidebarProvider>\n  )\n}\n","type":"registry:component","target":"components/ward/dashboard/dashboard.tsx"},{"path":"registry/ward/blocks/dashboard/app-sidebar.tsx","content":"import * as React from \"react\"\nimport {\n  ChartColumnIcon,\n  CircleHelpIcon,\n  FolderIcon,\n  LayersIcon,\n  LayoutDashboardIcon,\n  SettingsIcon,\n  UsersIcon,\n} from \"lucide-react\"\n\nimport {\n  Sidebar,\n  SidebarContent,\n  SidebarFooter,\n  SidebarGroup,\n  SidebarGroupContent,\n  SidebarGroupLabel,\n  SidebarHeader,\n  SidebarMenu,\n  SidebarMenuBadge,\n  SidebarMenuButton,\n  SidebarMenuItem,\n} from \"@/registry/ward/ui/sidebar\"\n\n/*\n * Adapted from shadcn/ui (https://github.com/shadcn-ui/ui).\n *\n * MIT License\n *\n * Copyright (c) 2023 shadcn\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\n// Swap these links for your routes, and set isActive from the current path.\nconst groups = [\n  {\n    label: \"Manage\",\n    items: [\n      { title: \"Overview\", url: \"#\", icon: LayoutDashboardIcon, isActive: true },\n      { title: \"Items\", url: \"#\", icon: LayersIcon, badge: \"128\" },\n      { title: \"Collections\", url: \"#\", icon: FolderIcon },\n      { title: \"Members\", url: \"#\", icon: UsersIcon },\n    ],\n  },\n  {\n    label: \"Reports\",\n    items: [{ title: \"Analytics\", url: \"#\", icon: ChartColumnIcon }],\n  },\n]\n\nconst footer = [\n  { title: \"Settings\", url: \"#\", icon: SettingsIcon },\n  { title: \"Help\", url: \"#\", icon: CircleHelpIcon },\n]\n\nexport function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {\n  return (\n    <Sidebar collapsible=\"offcanvas\" {...props}>\n      <SidebarHeader>\n        <SidebarMenu>\n          <SidebarMenuItem>\n            <SidebarMenuButton asChild className=\"data-[slot=sidebar-menu-button]:p-1.5!\">\n              <a href=\"#\">\n                <span className=\"flex size-6 items-center justify-center rounded-md bg-sidebar-primary text-xs font-semibold text-sidebar-primary-foreground\">\n                  W\n                </span>\n                <span className=\"text-base font-semibold\">Ward</span>\n              </a>\n            </SidebarMenuButton>\n          </SidebarMenuItem>\n        </SidebarMenu>\n      </SidebarHeader>\n      <SidebarContent>\n        {groups.map((group) => (\n          <SidebarGroup key={group.label}>\n            <SidebarGroupLabel>{group.label}</SidebarGroupLabel>\n            <SidebarGroupContent>\n              <SidebarMenu>\n                {group.items.map((item) => (\n                  <SidebarMenuItem key={item.title}>\n                    <SidebarMenuButton asChild isActive={item.isActive}>\n                      <a href={item.url}>\n                        <item.icon />\n                        <span>{item.title}</span>\n                      </a>\n                    </SidebarMenuButton>\n                    {item.badge && <SidebarMenuBadge>{item.badge}</SidebarMenuBadge>}\n                  </SidebarMenuItem>\n                ))}\n              </SidebarMenu>\n            </SidebarGroupContent>\n          </SidebarGroup>\n        ))}\n      </SidebarContent>\n      <SidebarFooter>\n        <SidebarMenu>\n          {footer.map((item) => (\n            <SidebarMenuItem key={item.title}>\n              <SidebarMenuButton asChild>\n                <a href={item.url}>\n                  <item.icon />\n                  <span>{item.title}</span>\n                </a>\n              </SidebarMenuButton>\n            </SidebarMenuItem>\n          ))}\n        </SidebarMenu>\n      </SidebarFooter>\n    </Sidebar>\n  )\n}\n","type":"registry:component","target":"components/ward/dashboard/app-sidebar.tsx"},{"path":"registry/ward/blocks/dashboard/site-header.tsx","content":"import { Separator } from \"@/registry/ward/ui/separator\"\nimport { SidebarTrigger } from \"@/registry/ward/ui/sidebar\"\n\n/*\n * Adapted from shadcn/ui (https://github.com/shadcn-ui/ui).\n *\n * MIT License\n *\n * Copyright (c) 2023 shadcn\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\n/** The bar above the page: the sidebar toggle and the page title. */\nexport function SiteHeader({ title }: { title: string }) {\n  return (\n    <header className=\"flex h-12 shrink-0 items-center gap-2 border-b border-border px-4 lg:px-6\">\n      <SidebarTrigger className=\"-ml-1\" />\n      <Separator\n        orientation=\"vertical\"\n        className=\"mx-2 data-[orientation=vertical]:h-4\"\n      />\n      <h1 className=\"text-base font-medium\">{title}</h1>\n    </header>\n  )\n}\n","type":"registry:component","target":"components/ward/dashboard/site-header.tsx"},{"path":"registry/ward/blocks/dashboard/section-cards.tsx","content":"import { TrendingDownIcon, TrendingUpIcon } from \"lucide-react\"\n\nimport { Badge } from \"@/registry/ward/ui/badge\"\nimport {\n  Card,\n  CardAction,\n  CardDescription,\n  CardFooter,\n  CardHeader,\n  CardTitle,\n} from \"@/registry/ward/ui/card\"\n\n/*\n * Adapted from shadcn/ui (https://github.com/shadcn-ui/ui).\n *\n * MIT License\n *\n * Copyright (c) 2023 shadcn\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\n/** A row of stat cards. A `change` that starts with a minus sign shows a falling trend, any other a rising one. */\nexport function SectionCards({\n  cards,\n}: {\n  cards: { label: string; value: string; change?: string; note?: string }[]\n}) {\n  // The row sizes itself to its own width, so it needs no help from the page around it.\n  return (\n    <div className=\"@container/cards\">\n      <div className=\"grid grid-cols-1 gap-4 @md/cards:grid-cols-2 @4xl/cards:grid-cols-4\">\n        {cards.map((card) => {\n          const TrendIcon = /^[-−]/.test(card.change ?? \"\")\n            ? TrendingDownIcon\n            : TrendingUpIcon\n          return (\n            <Card key={card.label} className=\"@container/card\">\n              <CardHeader>\n                <CardDescription>{card.label}</CardDescription>\n                <CardTitle className=\"text-2xl font-semibold tabular-nums @[250px]/card:text-3xl\">\n                  {card.value}\n                </CardTitle>\n                {card.change && (\n                  <CardAction>\n                    <Badge variant=\"outline\">\n                      <TrendIcon />\n                      {card.change}\n                    </Badge>\n                  </CardAction>\n                )}\n              </CardHeader>\n              {card.note && (\n                <CardFooter className=\"text-sm text-muted-foreground\">\n                  {card.note}\n                </CardFooter>\n              )}\n            </Card>\n          )\n        })}\n      </div>\n    </div>\n  )\n}\n","type":"registry:component","target":"components/ward/dashboard/section-cards.tsx"},{"path":"registry/ward/blocks/dashboard/chart-area.tsx","content":"\"use client\"\n\n/*\n * Adapted from shadcn/ui (https://github.com/shadcn-ui/ui).\n *\n * MIT License\n *\n * Copyright (c) 2023 shadcn\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\nimport * as React from \"react\"\nimport { Area, AreaChart, CartesianGrid, XAxis } from \"recharts\"\n\nimport {\n  Card,\n  CardAction,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle,\n} from \"@/registry/ward/ui/card\"\nimport {\n  ChartContainer,\n  ChartLegend,\n  ChartLegendContent,\n  ChartTooltip,\n  ChartTooltipContent,\n  type ChartConfig,\n} from \"@/registry/ward/ui/chart\"\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/registry/ward/ui/select\"\nimport { Tabs, TabsList, TabsTrigger } from \"@/registry/ward/ui/tabs\"\n\nconst DAY = 24 * 60 * 60 * 1000\n\nconst ranges = [\n  { value: \"90d\", days: 90, label: \"90 days\" },\n  { value: \"30d\", days: 30, label: \"30 days\" },\n  { value: \"7d\", days: 7, label: \"7 days\" },\n]\n\nconst MONTHS = [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\"]\n\n// Turns an ISO day such as 2026-09-28 into \"28 Sep\". Read in UTC and spelt out here, not by Intl,\n// so the server and every browser print the same text.\nexport function formatDate(value: string) {\n  const date = new Date(value)\n  return `${date.getUTCDate()} ${MONTHS[date.getUTCMonth()]}`\n}\n\n/**\n * A stacked area chart with a date range switch. Each key in `config` is a series in `data`,\n * drawn in the order given, and its colour comes from the config.\n */\nexport function ChartArea({\n  data,\n  config,\n  title = \"Activity\",\n  description,\n}: {\n  data: { date: string; [series: string]: number | string }[]\n  config: ChartConfig\n  title?: string\n  description?: string\n}) {\n  const [range, setRange] = React.useState(\"90d\")\n  const chartId = React.useId().replace(/[^a-zA-Z0-9_-]/g, \"\")\n  const series = Object.keys(config)\n\n  // Ranges count back from the newest row, so the chart works with any data, not only today's.\n  const days = (ranges.find((item) => item.value === range) ?? ranges[0]).days\n  const newest = Date.parse(data[data.length - 1]?.date ?? \"\")\n  const visible = data.filter(\n    (row) => Date.parse(row.date) > newest - days * DAY\n  )\n\n  return (\n    <Card className=\"@container/card\">\n      <CardHeader>\n        <CardTitle>{title}</CardTitle>\n        <CardDescription>{description ?? `Last ${days} days`}</CardDescription>\n        <CardAction>\n          <Tabs value={range} onValueChange={setRange}>\n            <TabsList\n              aria-label=\"Date range\"\n              className=\"hidden @[540px]/card:inline-flex\"\n            >\n              {ranges.map((item) => (\n                <TabsTrigger key={item.value} value={item.value}>\n                  {item.label}\n                </TabsTrigger>\n              ))}\n            </TabsList>\n          </Tabs>\n          <Select value={range} onValueChange={setRange}>\n            <SelectTrigger\n              size=\"sm\"\n              aria-label=\"Date range\"\n              className=\"w-28 @[540px]/card:hidden\"\n            >\n              <SelectValue />\n            </SelectTrigger>\n            <SelectContent align=\"end\">\n              {ranges.map((item) => (\n                <SelectItem key={item.value} value={item.value}>\n                  {item.label}\n                </SelectItem>\n              ))}\n            </SelectContent>\n          </Select>\n        </CardAction>\n      </CardHeader>\n      <CardContent className=\"px-2 sm:px-6\">\n        <ChartContainer config={config} className=\"aspect-auto h-[250px] w-full\">\n          <AreaChart data={visible}>\n            <defs>\n              {series.map((key) => (\n                <linearGradient\n                  key={key}\n                  id={`${chartId}-${key}`}\n                  x1=\"0\"\n                  y1=\"0\"\n                  x2=\"0\"\n                  y2=\"1\"\n                >\n                  <stop\n                    offset=\"5%\"\n                    stopColor={`var(--color-${key})`}\n                    stopOpacity={0.6}\n                  />\n                  <stop\n                    offset=\"95%\"\n                    stopColor={`var(--color-${key})`}\n                    stopOpacity={0.05}\n                  />\n                </linearGradient>\n              ))}\n            </defs>\n            <CartesianGrid vertical={false} />\n            <XAxis\n              dataKey=\"date\"\n              tickLine={false}\n              axisLine={false}\n              tickMargin={8}\n              minTickGap={32}\n              tickFormatter={formatDate}\n            />\n            <ChartTooltip\n              cursor={false}\n              content={\n                <ChartTooltipContent\n                  labelFormatter={(value) => formatDate(String(value))}\n                  indicator=\"dot\"\n                />\n              }\n            />\n            {series.map((key) => (\n              <Area\n                key={key}\n                dataKey={key}\n                type=\"monotone\"\n                stackId=\"a\"\n                stroke={`var(--color-${key})`}\n                fill={`url(#${chartId}-${key})`}\n              />\n            ))}\n            <ChartLegend content={<ChartLegendContent />} />\n          </AreaChart>\n        </ChartContainer>\n      </CardContent>\n    </Card>\n  )\n}\n","type":"registry:component","target":"components/ward/dashboard/chart-area.tsx"},{"path":"registry/ward/blocks/dashboard/data-table.tsx","content":"\"use client\"\n\n/*\n * Adapted from shadcn/ui (https://github.com/shadcn-ui/ui).\n *\n * MIT License\n *\n * Copyright (c) 2023 shadcn\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\nimport * as React from \"react\"\nimport {\n  flexRender,\n  getCoreRowModel,\n  getFilteredRowModel,\n  getPaginationRowModel,\n  getSortedRowModel,\n  useReactTable,\n  type Column,\n  type ColumnDef,\n  type ColumnFiltersState,\n  type SortingState,\n  type VisibilityState,\n} from \"@tanstack/react-table\"\nimport {\n  ArrowDownIcon,\n  ArrowUpDownIcon,\n  ArrowUpIcon,\n  ChevronDownIcon,\n  Columns3Icon,\n} from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/registry/ward/ui/button\"\nimport { Checkbox } from \"@/registry/ward/ui/checkbox\"\nimport {\n  DropdownMenu,\n  DropdownMenuCheckboxItem,\n  DropdownMenuContent,\n  DropdownMenuTrigger,\n} from \"@/registry/ward/ui/dropdown-menu\"\nimport { Input } from \"@/registry/ward/ui/input\"\nimport {\n  Table,\n  TableBody,\n  TableCell,\n  TableHead,\n  TableHeader,\n  TableRow,\n} from \"@/registry/ward/ui/table\"\n\nconst PAGE_SIZE = 10\n\n/** A header button that sorts its column. Use it as a column's `header`. */\nexport function SortableHeader<T>({\n  column,\n  className,\n  children,\n}: {\n  column: Column<T, unknown>\n  className?: string\n  children: React.ReactNode\n}) {\n  const sorted = column.getIsSorted()\n  const Icon =\n    sorted === \"asc\"\n      ? ArrowUpIcon\n      : sorted === \"desc\"\n        ? ArrowDownIcon\n        : ArrowUpDownIcon\n\n  return (\n    <Button\n      variant=\"ghost\"\n      size=\"sm\"\n      className={cn(\"-ml-2.5\", className)}\n      onClick={() => column.toggleSorting(sorted === \"asc\")}\n    >\n      {children}\n      <Icon className={sorted ? undefined : \"opacity-50\"} />\n    </Button>\n  )\n}\n\n// The checkbox column comes with the table, so a caller's columns describe only its own data.\nfunction selectColumn<T>(): ColumnDef<T> {\n  return {\n    id: \"select\",\n    header: ({ table }) => (\n      <Checkbox\n        checked={table.getIsAllPageRowsSelected()}\n        onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}\n        aria-label=\"Select all rows on this page\"\n      />\n    ),\n    cell: ({ row }) => (\n      <Checkbox\n        checked={row.getIsSelected()}\n        onCheckedChange={(value) => row.toggleSelected(!!value)}\n        aria-label=\"Select row\"\n      />\n    ),\n    enableSorting: false,\n    enableHiding: false,\n  }\n}\n\nexport function DataTable<T>({\n  columns,\n  data,\n  searchKey,\n}: {\n  columns: ColumnDef<T>[]\n  data: T[]\n  /** The id of the column the search box filters. Leave it out for no search box. */\n  searchKey?: string\n}) {\n  const [sorting, setSorting] = React.useState<SortingState>([])\n  const [columnFilters, setColumnFilters] =\n    React.useState<ColumnFiltersState>([])\n  const [columnVisibility, setColumnVisibility] =\n    React.useState<VisibilityState>({})\n  const [rowSelection, setRowSelection] = React.useState({})\n  const allColumns = React.useMemo(\n    () => [selectColumn<T>(), ...columns],\n    [columns]\n  )\n\n  // React Compiler skips this component by itself, since TanStack Table returns functions it can't memoize safely.\n  // eslint-disable-next-line react-hooks/incompatible-library\n  const table = useReactTable({\n    data,\n    columns: allColumns,\n    state: { sorting, columnFilters, columnVisibility, rowSelection },\n    initialState: { pagination: { pageSize: PAGE_SIZE } },\n    onSortingChange: setSorting,\n    onColumnFiltersChange: setColumnFilters,\n    onColumnVisibilityChange: setColumnVisibility,\n    onRowSelectionChange: setRowSelection,\n    getCoreRowModel: getCoreRowModel(),\n    getSortedRowModel: getSortedRowModel(),\n    getFilteredRowModel: getFilteredRowModel(),\n    getPaginationRowModel: getPaginationRowModel(),\n  })\n\n  const searchColumn = searchKey ? table.getColumn(searchKey) : undefined\n  const { pageIndex } = table.getState().pagination\n\n  return (\n    <div className=\"flex flex-col gap-4\">\n      <div className=\"flex items-center gap-2\">\n        {searchColumn && (\n          <Input\n            placeholder={`Search by ${searchColumn.id}…`}\n            aria-label={`Search by ${searchColumn.id}`}\n            value={(searchColumn.getFilterValue() as string | undefined) ?? \"\"}\n            onChange={(event) => searchColumn.setFilterValue(event.target.value)}\n            className=\"max-w-sm\"\n          />\n        )}\n        <DropdownMenu>\n          <DropdownMenuTrigger asChild>\n            <Button variant=\"outline\" size=\"sm\" className=\"ml-auto\">\n              <Columns3Icon />\n              Columns\n              <ChevronDownIcon />\n            </Button>\n          </DropdownMenuTrigger>\n          <DropdownMenuContent align=\"end\">\n            {table\n              .getAllColumns()\n              .filter((column) => column.getCanHide())\n              .map((column) => (\n                <DropdownMenuCheckboxItem\n                  key={column.id}\n                  className=\"capitalize\"\n                  checked={column.getIsVisible()}\n                  onCheckedChange={(value) => column.toggleVisibility(value)}\n                >\n                  {column.id}\n                </DropdownMenuCheckboxItem>\n              ))}\n          </DropdownMenuContent>\n        </DropdownMenu>\n      </div>\n      <div className=\"overflow-hidden rounded-lg border border-border\">\n        <Table>\n          <TableHeader className=\"bg-muted/50\">\n            {table.getHeaderGroups().map((headerGroup) => (\n              <TableRow key={headerGroup.id}>\n                {headerGroup.headers.map((header) => {\n                  const sorted = header.column.getIsSorted()\n                  return (\n                    <TableHead\n                      key={header.id}\n                      colSpan={header.colSpan}\n                      aria-sort={\n                        sorted\n                          ? sorted === \"asc\"\n                            ? \"ascending\"\n                            : \"descending\"\n                          : undefined\n                      }\n                      className={header.column.id === \"select\" ? \"w-10\" : undefined}\n                    >\n                      {header.isPlaceholder\n                        ? null\n                        : flexRender(\n                            header.column.columnDef.header,\n                            header.getContext()\n                          )}\n                    </TableHead>\n                  )\n                })}\n              </TableRow>\n            ))}\n          </TableHeader>\n          <TableBody>\n            {table.getRowModel().rows.length ? (\n              table.getRowModel().rows.map((row) => (\n                <TableRow\n                  key={row.id}\n                  data-state={row.getIsSelected() ? \"selected\" : undefined}\n                >\n                  {row.getVisibleCells().map((cell) => (\n                    <TableCell key={cell.id}>\n                      {flexRender(\n                        cell.column.columnDef.cell,\n                        cell.getContext()\n                      )}\n                    </TableCell>\n                  ))}\n                </TableRow>\n              ))\n            ) : (\n              <TableRow>\n                <TableCell\n                  colSpan={table.getVisibleLeafColumns().length}\n                  className=\"h-24 text-center text-muted-foreground\"\n                >\n                  No results.\n                </TableCell>\n              </TableRow>\n            )}\n          </TableBody>\n        </Table>\n      </div>\n      <div className=\"flex flex-wrap items-center justify-between gap-2\">\n        <p className=\"text-sm text-muted-foreground\">\n          {table.getFilteredSelectedRowModel().rows.length} of{\" \"}\n          {table.getFilteredRowModel().rows.length} selected\n        </p>\n        <div className=\"flex items-center gap-2\">\n          <span className=\"mr-2 text-sm font-medium\">\n            Page {pageIndex + 1} of {Math.max(table.getPageCount(), 1)}\n          </span>\n          <Button\n            variant=\"outline\"\n            size=\"sm\"\n            onClick={() => table.previousPage()}\n            disabled={!table.getCanPreviousPage()}\n          >\n            Previous\n          </Button>\n          <Button\n            variant=\"outline\"\n            size=\"sm\"\n            onClick={() => table.nextPage()}\n            disabled={!table.getCanNextPage()}\n          >\n            Next\n          </Button>\n        </div>\n      </div>\n    </div>\n  )\n}\n","type":"registry:component","target":"components/ward/dashboard/data-table.tsx"},{"path":"registry/ward/blocks/dashboard/data.tsx","content":"import type { ColumnDef } from \"@tanstack/react-table\"\n\nimport { Badge } from \"@/registry/ward/ui/badge\"\nimport type { ChartConfig } from \"@/registry/ward/ui/chart\"\nimport { formatDate } from \"./chart-area\"\nimport { SortableHeader } from \"./data-table\"\n\n/*\n * Adapted from shadcn/ui (https://github.com/shadcn-ui/ui).\n *\n * MIT License\n *\n * Copyright (c) 2023 shadcn\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\n// Sample content for the block. Replace all of it with your own data.\n\nexport const cards = [\n  { label: \"Items published\", value: \"1,284\", change: \"+12.5%\", note: \"More than the last 30 days\" },\n  { label: \"Clicks\", value: \"48,210\", change: \"+8.2%\", note: \"Across websites and apps\" },\n  { label: \"New members\", value: \"326\", change: \"-4.1%\", note: \"Fewer sign-ups than last month\" },\n  { label: \"In review\", value: \"17\", note: \"Waiting for a curator\" },\n]\n\n// Greyscale like the rest of Ward: the two series differ in lightness, and the legend names them.\nexport const chartConfig = {\n  websites: { label: \"Websites\", color: \"var(--chart-1)\" },\n  apps: { label: \"Apps\", color: \"var(--chart-3)\" },\n} satisfies ChartConfig\n\n// 90 days of made-up clicks. Computed from the day number, so the server and the browser draw the same chart.\nconst DAY = 24 * 60 * 60 * 1000\nconst FIRST_DAY = Date.UTC(2026, 6, 1)\nconst week = (day: number, shift = 0) => Math.sin((day * 2 * Math.PI) / 7 + shift)\nexport const chartData = Array.from({ length: 90 }, (_, day) => ({\n  date: new Date(FIRST_DAY + day * DAY).toISOString().slice(0, 10),\n  websites: Math.round(240 + day * 1.6 + 45 * Math.sin(day / 6) + 22 * week(day)),\n  apps: Math.round(120 + day * 0.8 + 20 * Math.sin(day / 5 + 1) + 12 * week(day, 1)),\n}))\n\nexport type Row = {\n  id: string\n  title: string\n  kind: \"Website\" | \"App\" | \"Component\" | \"Design\" | \"Tool\" | \"Skill\"\n  status: \"Live\" | \"In review\" | \"Draft\"\n  clicks: number\n  updated: string\n}\n\nexport const rows: Row[] = [\n  { id: \"1\", title: \"Onboarding checklist\", kind: \"Component\", status: \"Live\", clicks: 4210, updated: \"2026-09-27\" },\n  { id: \"2\", title: \"Pricing page with a billing toggle\", kind: \"Website\", status: \"Live\", clicks: 3894, updated: \"2026-09-26\" },\n  { id: \"3\", title: \"Habit tracker\", kind: \"App\", status: \"Live\", clicks: 3502, updated: \"2026-09-25\" },\n  { id: \"4\", title: \"Command palette\", kind: \"Component\", status: \"Live\", clicks: 3188, updated: \"2026-09-24\" },\n  { id: \"5\", title: \"Portfolio with a full-bleed hero\", kind: \"Website\", status: \"In review\", clicks: 0, updated: \"2026-09-28\" },\n  { id: \"6\", title: \"Empty states in monochrome\", kind: \"Design\", status: \"Live\", clicks: 2764, updated: \"2026-09-22\" },\n  { id: \"7\", title: \"Recipe planner\", kind: \"App\", status: \"Live\", clicks: 2430, updated: \"2026-09-21\" },\n  { id: \"8\", title: \"Changelog feed\", kind: \"Website\", status: \"Live\", clicks: 2217, updated: \"2026-09-20\" },\n  { id: \"9\", title: \"Code review helper\", kind: \"Skill\", status: \"Draft\", clicks: 0, updated: \"2026-09-28\" },\n  { id: \"10\", title: \"Settings with grouped sections\", kind: \"Design\", status: \"Live\", clicks: 1986, updated: \"2026-09-19\" },\n  { id: \"11\", title: \"Screenshot compressor\", kind: \"Tool\", status: \"Live\", clicks: 1873, updated: \"2026-09-18\" },\n  { id: \"12\", title: \"Date range picker\", kind: \"Component\", status: \"Live\", clicks: 1655, updated: \"2026-09-17\" },\n  { id: \"13\", title: \"Budget dashboard\", kind: \"App\", status: \"In review\", clicks: 0, updated: \"2026-09-27\" },\n  { id: \"14\", title: \"Documentation landing page\", kind: \"Website\", status: \"Live\", clicks: 1421, updated: \"2026-09-15\" },\n  { id: \"15\", title: \"Migration guide writer\", kind: \"Skill\", status: \"Live\", clicks: 1302, updated: \"2026-09-14\" },\n  { id: \"16\", title: \"Notification centre\", kind: \"Design\", status: \"Draft\", clicks: 0, updated: \"2026-09-26\" },\n  { id: \"17\", title: \"Colour contrast checker\", kind: \"Tool\", status: \"Live\", clicks: 1196, updated: \"2026-09-12\" },\n  { id: \"18\", title: \"Data table with filters\", kind: \"Component\", status: \"Live\", clicks: 1088, updated: \"2026-09-11\" },\n  { id: \"19\", title: \"Meditation timer\", kind: \"App\", status: \"Live\", clicks: 964, updated: \"2026-09-09\" },\n  { id: \"20\", title: \"Event ticketing site\", kind: \"Website\", status: \"In review\", clicks: 0, updated: \"2026-09-25\" },\n]\n\nconst statusVariant = { Live: \"default\", \"In review\": \"secondary\", Draft: \"outline\" } as const\n\nexport const columns: ColumnDef<Row>[] = [\n  {\n    accessorKey: \"title\",\n    header: ({ column }) => <SortableHeader column={column}>Title</SortableHeader>,\n    cell: ({ row }) => <span className=\"font-medium\">{row.original.title}</span>,\n    enableHiding: false,\n  },\n  {\n    accessorKey: \"kind\",\n    header: \"Kind\",\n    cell: ({ row }) => <Badge variant=\"outline\">{row.original.kind}</Badge>,\n  },\n  {\n    accessorKey: \"status\",\n    header: \"Status\",\n    cell: ({ row }) => <Badge variant={statusVariant[row.original.status]}>{row.original.status}</Badge>,\n  },\n  {\n    accessorKey: \"clicks\",\n    header: ({ column }) => (\n      <div className=\"flex justify-end\">\n        <SortableHeader column={column} className=\"-mr-2.5 ml-0\">Clicks</SortableHeader>\n      </div>\n    ),\n    cell: ({ row }) => <div className=\"text-right tabular-nums\">{row.original.clicks.toLocaleString(\"en-GB\")}</div>,\n  },\n  {\n    accessorKey: \"updated\",\n    header: ({ column }) => <SortableHeader column={column}>Updated</SortableHeader>,\n    cell: ({ row }) => <span className=\"text-muted-foreground\">{formatDate(row.original.updated)}</span>,\n  },\n]\n","type":"registry:component","target":"components/ward/dashboard/data.tsx"}],"meta":{"variants":["Default"],"version":"1.0.0","source":"https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/new-york-v4/blocks/dashboard-01/page.tsx","access":"free"},"categories":["blocks"],"type":"registry:block"}