{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"list","type":"registry:ui","title":"List","description":"Rows of people, files or things: a picture, a title, details and actions.","dependencies":["lucide-react"],"registryDependencies":["https://dashral.com/r/utils.json","https://dashral.com/r/lib-virtual-rows.json"],"files":[{"path":"registry/dashral/list.tsx","type":"registry:file","target":"components/dashral/list.tsx","content":"import * as React from \"react\"\nimport { ChevronRightIcon } from \"lucide-react\"\n\nimport { cn } from \"./lib/utils\"\nimport { VirtualRows } from \"./lib/virtual-rows\"\n\ntype ListItem = {\n  /** A stable key; the index is used without one. */\n  key?: string\n  /** The row's main line: a name, a title. */\n  title: React.ReactNode\n  /** A second line under it. */\n  description?: React.ReactNode\n  /** At the start: an Avatar, an icon in a tile, a logo. */\n  media?: React.ReactNode\n  /** At the end, quiet: a date, a status Badge, a count. */\n  meta?: React.ReactNode\n  /** At the very end: buttons or a menu. They stay clickable on a row that links. */\n  actions?: React.ReactNode\n  /** Makes the whole row a link (through linkAs); a chevron shows at its end unless it has actions. */\n  href?: string\n  /** Makes the whole row a button. */\n  onClick?: () => void\n  /** The row you're on: tinted, and announced as current. */\n  active?: boolean\n  disabled?: boolean\n}\n\ntype ListProps = Omit<React.ComponentProps<\"ul\">, \"children\"> & {\n  items: ListItem[]\n  /** default: rows on the page; card: inside a bordered card of their own. */\n  variant?: \"default\" | \"card\"\n  /** Lines between the rows. On by default. */\n  divided?: boolean\n  /** sm for a popover, a sidebar or a dense panel. */\n  size?: \"sm\" | \"default\"\n  /** Shown in place of the rows when there are none, e.g. an EmptyState. */\n  empty?: React.ReactNode\n  /** Your router's link, for rows with href. */\n  linkAs?: React.ElementType\n  /**\n   * Draws only the rows in view, in a box that scrolls, so a list of thousands (a log, an audit trail) stays smooth.\n   * Rows of any height work. Needs List inside a client component.\n   */\n  virtual?: boolean\n  /** With virtual: the box's height in pixels. */\n  height?: number\n  /** With virtual: called as the last rows come into view, to fetch the next ones and add them to items. */\n  onLoadMore?: () => Promise<void> | void\n  /** With onLoadMore: false once there's nothing more to load. */\n  hasMore?: boolean\n  classNames?: { item?: string; media?: string; title?: string; description?: string; meta?: string; actions?: string }\n}\n\n/**\n * Rows of people, files or things, in one tag: a picture, a title and description, details and actions at the end.\n * Each row can link or act as a whole while its own buttons still work. Where it's narrow (a phone, a sidebar, a small\n * card) the details and actions move under the text instead of squeezing it: it follows its own width, not the\n * screen's.\n */\nfunction List({\n  items,\n  variant = \"default\",\n  divided = true,\n  size = \"default\",\n  empty,\n  linkAs: Link = \"a\",\n  virtual = false,\n  height = 480,\n  onLoadMore,\n  hasMore = true,\n  className,\n  classNames,\n  ...props\n}: ListProps) {\n  const sm = size === \"sm\"\n  if (items.length === 0 && empty !== undefined) return <>{empty}</>\n\n  const renderRow = (item: ListItem, i: number) => {\n    const interactive = item.href !== undefined || item.onClick !== undefined\n    const trailing = item.meta !== undefined || item.actions !== undefined || (item.href !== undefined && !item.actions)\n    // Only a chevron at the end: it stays there at any width; there's nothing to move under the text.\n    const chevronOnly = item.meta === undefined && item.actions === undefined && item.href !== undefined\n    // The title carries the row's link or button; its ::after stretches over the whole row, so a click anywhere\n    // lands on it, while the actions sit above that layer and keep their own clicks.\n    const stretch =\n      \"outline-none after:absolute after:inset-0 after:content-[''] focus-visible:after:rounded-[inherit] focus-visible:after:ring-2 focus-visible:after:ring-ring/50 focus-visible:after:ring-inset\"\n    const titleText = (\n      <span className={cn(\"block truncate font-medium\", classNames?.title)}>{item.title}</span>\n    )\n    const title =\n      item.href !== undefined && !item.disabled ? (\n        <Link href={item.href} aria-current={item.active ? \"page\" : undefined} data-list-link=\"\" className={stretch}>\n          {titleText}\n        </Link>\n      ) : item.onClick && !item.disabled ? (\n        <button type=\"button\" onClick={item.onClick} aria-current={item.active || undefined} data-list-link=\"\" className={cn(\"w-full text-start\", stretch)}>\n          {titleText}\n        </button>\n      ) : (\n        titleText\n      )\n\n    return (\n      <li\n        key={item.key ?? i}\n        data-slot=\"list-item\"\n        data-active={item.active || undefined}\n        data-disabled={item.disabled || undefined}\n        className={cn(\n          \"relative grid items-center gap-x-3 gap-y-2\",\n          item.media ? \"grid-cols-[auto_minmax(0,1fr)]\" : \"grid-cols-[minmax(0,1fr)]\",\n          chevronOnly\n            ? item.media\n              ? \"grid-cols-[auto_minmax(0,1fr)_auto]\"\n              : \"grid-cols-[minmax(0,1fr)_auto]\"\n            : trailing && (item.media ? \"@sm:grid-cols-[auto_minmax(0,1fr)_auto]\" : \"@sm:grid-cols-[minmax(0,1fr)_auto]\"),\n          sm ? \"px-3 py-2\" : \"px-4 py-3\",\n          variant === \"default\" && !divided && \"rounded-lg\",\n          interactive && !item.disabled && \"transition-colors duration-(--duration-fast) hover:bg-accent/50\",\n          item.active && \"bg-accent/60\",\n          item.disabled && \"opacity-50\",\n          classNames?.item\n        )}\n      >\n        {item.media && <div className={cn(\"flex shrink-0 items-center\", classNames?.media)}>{item.media}</div>}\n        {/* One column that can't outgrow the row: a title inside a link or button truncates instead of running under\n            the details beside it. */}\n        <div className=\"grid min-w-0 grid-cols-[minmax(0,1fr)] gap-0.5\">\n          {title}\n          {item.description && (\n            <span className={cn(\"truncate text-muted-foreground\", sm ? \"text-xs\" : \"text-[13px]\", classNames?.description)}>{item.description}</span>\n          )}\n        </div>\n        {trailing && (\n          // Under the text where it's narrow (in line with it, after the media), at the end where there's room.\n          <div\n            className={cn(\n              \"flex min-w-0 flex-wrap items-center gap-x-3 gap-y-2 @sm:flex-nowrap @sm:justify-end\",\n              chevronOnly ? \"justify-end\" : item.media && \"col-start-2 @sm:col-start-3\"\n            )}\n          >\n            {item.meta !== undefined && (\n              <div className={cn(\"flex items-center gap-2 text-xs text-muted-foreground tabular-nums\", classNames?.meta)}>{item.meta}</div>\n            )}\n            {item.actions !== undefined && (\n              <div className={cn(\"relative z-10 flex items-center gap-1.5 @max-sm:ms-auto\", classNames?.actions)}>{item.actions}</div>\n            )}\n            {item.href !== undefined && !item.actions && (\n              <ChevronRightIcon aria-hidden className=\"ms-auto size-4 text-muted-foreground rtl:-scale-x-100\" />\n            )}\n          </div>\n        )}\n      </li>\n    )\n  }\n\n  const card = \"overflow-hidden rounded-xl border bg-card text-card-foreground shadow-xs\"\n\n  if (virtual)\n    return (\n      <VirtualRows\n        role=\"list\"\n        data-slot=\"list\"\n        rows={items.map((item, i) =>\n          // Each row says where it is in the whole list, as only some are on the page.\n          React.cloneElement(renderRow(item, i), { \"aria-setsize\": items.length, \"aria-posinset\": i + 1 } as React.HTMLAttributes<HTMLLIElement>)\n        )}\n        height={height}\n        onLoadMore={onLoadMore}\n        hasMore={hasMore}\n        estimateSize={sm ? 45 : 57}\n        // The box takes your layout classes (its width, a margin), as the list itself does when it isn't virtual.\n        boxClassName={cn(variant === \"card\" && card, className)}\n        className={cn(\"@container grid\", divided && \"divide-y\", \"text-sm\")}\n        {...props}\n      />\n    )\n\n  return (\n    <ul\n      role=\"list\"\n      data-slot=\"list\"\n      className={cn(\n        \"@container grid\",\n        divided && \"divide-y\",\n        variant === \"card\" && \"overflow-hidden rounded-xl border bg-card text-card-foreground shadow-xs\",\n        \"text-sm\",\n        className\n      )}\n      {...props}\n    >\n      {items.map(renderRow)}\n    </ul>\n  )\n}\n\nexport { List, type ListProps, type ListItem }\n"}],"cssVars":{"theme":{"ease-standard":"var(--ease-standard)","ease-enter":"var(--ease-enter)","ease-exit":"var(--ease-exit)","ease-spring":"var(--ease-spring)"},"light":{"duration-fast":"160ms","duration-standard":"240ms","duration-considered":"480ms","ease-standard":"cubic-bezier(0.2, 0, 0, 1)","ease-enter":"cubic-bezier(0, 0, 0, 1)","ease-exit":"cubic-bezier(0.3, 0, 1, 1)","ease-spring":"linear(0, 0.071 4%, 0.234 8%, 0.433 12%, 0.626 17%, 0.791 21%, 0.917 25%, 1.004 29%, 1.055 33%, 1.079 38%, 1.083 42%, 1.075 46%, 1.06 50%, 1.043 54%, 1.027 58%, 1.015 62%, 1.005 67%, 0.998 71%, 0.995 75%, 0.993 79%, 0.993 83%, 0.994 88%, 0.995 92%, 0.997 96%, 1)"},"dark":{}}}