{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "records-table",
  "title": "Records table",
  "description": "CRM-style sortable and selectable records grid with tags, relationship status, and links.",
  "registryDependencies": [
    "https://beadkit.dev/r/ai-core.json"
  ],
  "files": [
    {
      "path": "components/ai/RecordsTable.tsx",
      "content": "\"use client\";\n\nimport { useMemo, useState, type ReactNode } from \"react\";\nimport { safeExternalHref } from \"./safeExternalHref\";\n\nexport type RelationshipStatus = \"strong\" | \"weak\" | \"very-weak\" | \"none\";\nexport interface RecordsTableRow { id: string; name: string; tags: readonly string[]; lastInteraction: string; relationship: RelationshipStatus; website?: string; }\nexport interface RecordsTableProps { rows: readonly RecordsTableRow[]; title?: ReactNode; onSelectionChange?: (ids: readonly string[]) => void; className?: string; }\ntype SortKey = \"name\" | \"lastInteraction\" | \"relationship\";\nconst rank: Record<RelationshipStatus, number> = { strong: 3, weak: 2, \"very-weak\": 1, none: 0 };\n\nfunction WebsiteLink({ website }: { website?: string }) {\n  const href = safeExternalHref(website);\n  if (!website) return <span className=\"text-muted-foreground\">—</span>;\n  if (!href) return <span className=\"block truncate text-muted-foreground\">{website}</span>;\n  return <a href={href} target=\"_blank\" rel=\"noreferrer\" className=\"block truncate text-foreground underline underline-offset-2\">{website}</a>;\n}\n\nexport function RecordsTable({ rows, title, onSelectionChange, className = \"\" }: RecordsTableProps) {\n  const [selected, setSelected] = useState<Set<string>>(new Set()); const [sort, setSort] = useState<{ key: SortKey; direction: 1 | -1 }>({ key: \"name\", direction: 1 });\n  const sorted = useMemo(() => [...rows].sort((a, b) => { const av = sort.key === \"name\" ? a.name : sort.key === \"lastInteraction\" ? a.lastInteraction : rank[a.relationship]; const bv = sort.key === \"name\" ? b.name : sort.key === \"lastInteraction\" ? b.lastInteraction : rank[b.relationship]; return (typeof av === \"string\" ? av.localeCompare(bv as string) : av - (bv as number)) * sort.direction; }), [rows, sort]);\n  const update = (next: Set<string>) => { setSelected(next); onSelectionChange?.([...next]); };\n  const toggle = (id: string) => { const next = new Set(selected); next.has(id) ? next.delete(id) : next.add(id); update(next); };\n  const all = sorted.length > 0 && sorted.every((row) => selected.has(row.id));\n  const sortBy = (key: SortKey) => setSort((current) => current.key === key ? { key, direction: current.direction === 1 ? -1 : 1 } : { key, direction: 1 });\n  return <div data-ai-table=\"records\" className={`w-full max-w-4xl ${className}`}><div className=\"overflow-hidden rounded-xl border bg-card shadow-sm\">{title ? <div className=\"border-b border-border px-4 py-3 text-sm font-medium\">{title}</div> : null}<div className=\"overflow-x-auto\" tabIndex={0} aria-label=\"Scrollable records table\"><table className=\"w-full min-w-[46rem] border-collapse text-left text-sm\"><thead><tr className=\"border-b border-border\"><th scope=\"col\" className=\"sticky left-0 bg-card px-4 py-2.5\"><input type=\"checkbox\" checked={all} aria-label=\"Select all records\" onChange={() => { const next = new Set(selected); sorted.forEach((row) => all ? next.delete(row.id) : next.add(row.id)); update(next); }} /></th>{([[\"name\", \"Name\"], [\"lastInteraction\", \"Last interaction\"], [\"relationship\", \"Relationship\"]] as const).map(([key, label]) => <th key={key} scope=\"col\" className=\"px-4 py-2.5 text-xs font-medium text-muted-foreground\"><button type=\"button\" onClick={() => sortBy(key)} className=\"rounded px-1 py-1 hover:bg-muted\" aria-label={`Sort by ${label}`}>{label} {sort.key === key ? (sort.direction === 1 ? \"↑\" : \"↓\") : \"\"}</button></th>)}<th scope=\"col\" className=\"px-4 py-2.5 text-xs font-medium text-muted-foreground\">Website</th></tr></thead><tbody>{sorted.map((row) => <tr key={row.id} className={`border-b border-border last:border-0 hover:bg-muted ${selected.has(row.id) ? \"bg-muted\" : \"\"}`}><td className=\"sticky left-0 bg-inherit px-4 py-3\"><input type=\"checkbox\" checked={selected.has(row.id)} aria-label={`Select ${row.name}`} onChange={() => toggle(row.id)} /></td><th scope=\"row\" className=\"px-4 py-3 font-medium\">{row.name}<div className=\"mt-1 flex flex-wrap gap-1\">{row.tags.map((tag) => <span key={tag} className=\"rounded-full bg-muted px-1.5 py-0.5 text-[11px] text-muted-foreground\">{tag}</span>)}</div></th><td className=\"px-4 py-3 text-muted-foreground\">{row.lastInteraction}</td><td className=\"px-4 py-3 text-muted-foreground\"><span className={`mr-1.5 inline-block size-1.5 rounded-full ${row.relationship === \"strong\" ? \"bg-success\" : row.relationship === \"none\" ? \"bg-muted-foreground\" : row.relationship === \"very-weak\" ? \"bg-destructive\" : \"bg-accent\"}`} aria-hidden=\"true\" />{row.relationship.replace(\"-\", \" \")}</td><td className=\"max-w-48 px-4 py-3\"><WebsiteLink website={row.website} /></td></tr>)}</tbody></table></div></div></div>;\n}\nexport default RecordsTable;\n",
      "type": "registry:component",
      "target": "@components/ai/RecordsTable.tsx"
    }
  ],
  "type": "registry:component"
}
