{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "table-17",
  "title": "Expandable rows",
  "description": "Rows that carry a note open below to show it.",
  "dependencies": [
    "@tanstack/react-table",
    "motion"
  ],
  "registryDependencies": [
    "https://ui.ndekocode.dev/r/animated-size.json",
    "https://ui.ndekocode.dev/r/badge.json",
    "https://ui.ndekocode.dev/r/button.json",
    "https://ui.ndekocode.dev/r/checkbox.json",
    "https://ui.ndekocode.dev/r/icons.json",
    "https://ui.ndekocode.dev/r/table.json",
    "https://ui.ndekocode.dev/r/theme.json",
    "https://ui.ndekocode.dev/r/use-prefers-reduced-motion.json",
    "https://ui.ndekocode.dev/r/utils.json"
  ],
  "files": [
    {
      "path": "registry/ndeko/components/table/table-17.tsx",
      "content": "'use client';\n\nimport {\n  type ColumnDef,\n  flexRender,\n  getCoreRowModel,\n  getExpandedRowModel,\n  type Row,\n  useReactTable,\n} from '@tanstack/react-table';\nimport { AnimatePresence, motion, type Variants } from 'motion/react';\nimport { Fragment } from 'react';\n\nimport { usePrefersReducedMotion } from '@/registry/ndeko/hooks/use-prefers-reduced-motion';\nimport { ChevronDownIcon, InfoIcon } from '@/registry/ndeko/lib/icons';\nimport { cn } from '@/registry/ndeko/lib/utils';\nimport { AnimatedSize } from '@/registry/ndeko/ui/animated-size';\nimport { Badge } from '@/registry/ndeko/ui/badge';\nimport { Button } from '@/registry/ndeko/ui/button';\nimport { Checkbox } from '@/registry/ndeko/ui/checkbox';\nimport {\n  Table,\n  TableBody,\n  TableCell,\n  TableHead,\n  TableHeader,\n  TableRow,\n} from '@/registry/ndeko/ui/table';\n\ntype Status = 'Active' | 'Inactive' | 'Pending';\n\ntype Account = {\n  id: string;\n  name: string;\n  email: string;\n  location: string;\n  status: Status;\n  balance: number;\n  note?: string;\n};\n\nconst STATUS_VARIANTS = {\n  Active: 'success',\n  Inactive: 'secondary',\n  Pending: 'warning',\n} as const;\n\nconst ACCOUNTS: Account[] = [\n  {\n    id: '1',\n    name: 'Jordan Hayes',\n    email: 'jordan.hayes@example.com',\n    location: 'Toronto, Canada',\n    status: 'Active',\n    balance: 1250,\n    note: 'Asked to move the renewal to the first week of December.',\n  },\n  {\n    id: '2',\n    name: 'Anna Novak',\n    email: 'anna.novak@example.com',\n    location: 'Prague, Czechia',\n    status: 'Active',\n    balance: 600,\n  },\n  {\n    id: '3',\n    name: 'Kenji Watanabe',\n    email: 'kenji.watanabe@example.com',\n    location: 'Osaka, Japan',\n    status: 'Inactive',\n    balance: 650,\n    note: 'Paused the account while the team is reorganised. Check in again in January.',\n  },\n  {\n    id: '4',\n    name: 'Lina Haddad',\n    email: 'lina.haddad@example.com',\n    location: 'Lyon, France',\n    status: 'Active',\n    balance: 0,\n  },\n  {\n    id: '5',\n    name: 'Ama Mensah',\n    email: 'ama.mensah@example.com',\n    location: 'Accra, Ghana',\n    status: 'Pending',\n    balance: -1000,\n    note: 'The last invoice bounced. A new card was requested on the billing page.',\n  },\n];\n\nconst CURRENCY = new Intl.NumberFormat('en-US', {\n  style: 'currency',\n  currency: 'USD',\n});\n\nconst FIGURE_COLUMNS = new Set(['balance']);\n\nconst NOTE_VARIANTS: Variants = {\n  hidden: {\n    opacity: 0,\n    transition: { duration: 0.12, ease: [0.23, 1, 0.32, 1] },\n  },\n  shown: {\n    opacity: 1,\n    transition: { duration: 0.2, ease: [0.23, 1, 0.32, 1] },\n  },\n};\n\nconst INSTANT_VARIANTS: Variants = {\n  hidden: { opacity: 0, transition: { duration: 0 } },\n  shown: { opacity: 1, transition: { duration: 0 } },\n};\n\nfunction expandLabelOf(row: Row<Account>) {\n  if (row.getIsExpanded()) {\n    return `Hide the note on ${row.original.name}`;\n  }\n  return `Show the note on ${row.original.name}`;\n}\n\nfunction ExpandButton({ row }: { row: Row<Account> }) {\n  if (!row.getCanExpand()) {\n    return null;\n  }\n  return (\n    <Button\n      variant='ghost'\n      size='icon-sm'\n      onClick={row.getToggleExpandedHandler()}\n      aria-expanded={row.getIsExpanded()}\n      aria-label={expandLabelOf(row)}\n      className='text-muted-foreground hover:text-foreground'\n    >\n      <ChevronDownIcon className='transition-transform duration-200 ease-(--ease-out-strong) group-aria-expanded/button:rotate-180' />\n    </Button>\n  );\n}\n\nconst COLUMNS: ColumnDef<Account>[] = [\n  {\n    id: 'expander',\n    header: () => <span className='sr-only'>Notes</span>,\n    cell: ({ row }) => <ExpandButton row={row} />,\n  },\n  {\n    id: 'select',\n    header: ({ table }) => (\n      <Checkbox\n        checked={table.getIsAllPageRowsSelected()}\n        indeterminate={table.getIsSomePageRowsSelected()}\n        onCheckedChange={(checked) => {\n          table.toggleAllPageRowsSelected(checked);\n        }}\n        aria-label='Select all rows'\n      />\n    ),\n    cell: ({ row }) => (\n      <Checkbox\n        checked={row.getIsSelected()}\n        onCheckedChange={(checked) => {\n          row.toggleSelected(checked);\n        }}\n        aria-label={`Select ${row.original.name}`}\n      />\n    ),\n  },\n  {\n    header: 'Name',\n    accessorKey: 'name',\n    cell: ({ row }) => <span className='font-medium'>{row.original.name}</span>,\n  },\n  {\n    header: 'Email',\n    accessorKey: 'email',\n    cell: ({ row }) => (\n      <span className='text-muted-foreground'>{row.original.email}</span>\n    ),\n  },\n  {\n    header: 'Location',\n    accessorKey: 'location',\n  },\n  {\n    header: 'Status',\n    accessorKey: 'status',\n    cell: ({ row }) => (\n      <Badge variant={STATUS_VARIANTS[row.original.status]}>\n        {row.original.status}\n      </Badge>\n    ),\n  },\n  {\n    header: 'Balance',\n    accessorKey: 'balance',\n    cell: ({ row }) => CURRENCY.format(row.original.balance),\n  },\n];\n\nfunction hasNote(row: Row<Account>) {\n  return Boolean(row.original.note);\n}\n\nfunction alignClassOf(columnId: string) {\n  if (FIGURE_COLUMNS.has(columnId)) {\n    return 'text-right tabular-nums';\n  }\n  if (columnId === 'expander') {\n    return 'w-px py-0 pe-0';\n  }\n  return 'whitespace-nowrap';\n}\n\nfunction rowStateOf(isSelected: boolean) {\n  if (isSelected) {\n    return 'selected';\n  }\n  return undefined;\n}\n\nfunction noteVariantsOf(shouldReduceMotion: boolean | null) {\n  if (shouldReduceMotion) {\n    return INSTANT_VARIANTS;\n  }\n  return NOTE_VARIANTS;\n}\n\nfunction NoteRow({ row }: { row: Row<Account> }) {\n  const shouldReduceMotion = usePrefersReducedMotion();\n  const variants = noteVariantsOf(shouldReduceMotion);\n  const isExpanded = row.getIsExpanded();\n  const columnCount = row.getVisibleCells().length;\n\n  return (\n    <TableRow\n      data-state={rowStateOf(row.getIsSelected())}\n      className='hover:bg-transparent'\n    >\n      <TableCell colSpan={columnCount} className='p-0'>\n        <AnimatedSize className='mx-0' contentClassName='px-0'>\n          <AnimatePresence initial={false}>\n            {isExpanded && (\n              <motion.div\n                key='note'\n                variants={variants}\n                initial='hidden'\n                animate='shown'\n                exit='hidden'\n                className='text-muted-foreground flex items-start gap-3 px-3 pb-3'\n              >\n                <span\n                  aria-hidden='true'\n                  className='flex h-5 w-7 shrink-0 items-center justify-center'\n                >\n                  <InfoIcon className='size-4' />\n                </span>\n                <p className='text-[13px] text-pretty'>{row.original.note}</p>\n              </motion.div>\n            )}\n          </AnimatePresence>\n        </AnimatedSize>\n      </TableCell>\n    </TableRow>\n  );\n}\n\nexport default function Table17() {\n  // eslint-disable-next-line react-hooks/incompatible-library -- TanStack Table v8 is required and cannot be memoized by the React Compiler.\n  const table = useReactTable({\n    data: ACCOUNTS,\n    columns: COLUMNS,\n    getRowCanExpand: hasNote,\n    getCoreRowModel: getCoreRowModel(),\n    getExpandedRowModel: getExpandedRowModel(),\n  });\n\n  return (\n    <div className='w-full'>\n      <Table>\n        <TableHeader>\n          {table.getHeaderGroups().map((headerGroup) => (\n            <TableRow key={headerGroup.id} className='hover:bg-transparent'>\n              {headerGroup.headers.map((header) => (\n                <TableHead\n                  key={header.id}\n                  className={alignClassOf(header.column.id)}\n                >\n                  {!header.isPlaceholder &&\n                    flexRender(\n                      header.column.columnDef.header,\n                      header.getContext()\n                    )}\n                </TableHead>\n              ))}\n            </TableRow>\n          ))}\n        </TableHeader>\n        <TableBody>\n          {table.getRowModel().rows.map((row) => {\n            const canExpand = row.getCanExpand();\n            return (\n              <Fragment key={row.id}>\n                <TableRow\n                  data-state={rowStateOf(row.getIsSelected())}\n                  className={cn(canExpand && 'border-b-0')}\n                >\n                  {row.getVisibleCells().map((cell) => (\n                    <TableCell\n                      key={cell.id}\n                      className={alignClassOf(cell.column.id)}\n                    >\n                      {flexRender(\n                        cell.column.columnDef.cell,\n                        cell.getContext()\n                      )}\n                    </TableCell>\n                  ))}\n                </TableRow>\n                {canExpand && <NoteRow row={row} />}\n              </Fragment>\n            );\n          })}\n        </TableBody>\n      </Table>\n    </div>\n  );\n}\n",
      "type": "registry:component"
    }
  ],
  "categories": [
    "components",
    "table"
  ],
  "type": "registry:component"
}