{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "claims",
  "title": "OIDC Claims Pieces",
  "description": "The shared pieces every provider form composes: the ClaimsList row editor for additional claims, the ClaimChip label marker, and the pure helpers that merge rows into a policy and read them back out.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "button",
    "field",
    "input"
  ],
  "files": [
    {
      "path": "registry/oidc/components/claims.tsx",
      "content": "\"use client\"\n\n// ClaimsList is a controlled composite input — mount it like any other input:\n// Controller in React Hook Form, form.Field in TanStack Form, plain state\n// otherwise. The pure helpers also work with useFieldArray-style rows.\n\nimport * as React from \"react\"\nimport { PlusIcon, Trash2Icon } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\nimport { FieldError, FieldLabel } from \"@/components/ui/field\"\nimport { Input } from \"@/components/ui/input\"\n\ntype ClaimRow = { name: string; values: string }\n\nfunction claimRowError(row: ClaimRow, reserved: string[] = []) {\n  const name = row.name.trim()\n  if (!name && !row.values.trim()) {\n    return null\n  }\n  if (!name) {\n    return \"Name the claim.\"\n  }\n  if (reserved.includes(name)) {\n    return \"This claim has a dedicated field above.\"\n  }\n  // Prototype-chain names silently vanish from plain-object claim maps.\n  if ([\"__proto__\", \"constructor\", \"prototype\"].includes(name)) {\n    return \"This claim name is not allowed.\"\n  }\n  if (!/^[\\w.-]+$/.test(name)) {\n    return \"Claim names: letters, digits, dots, dashes, underscores.\"\n  }\n  if (!row.values.trim()) {\n    return \"Add at least one accepted value.\"\n  }\n  return null\n}\n\nfunction claimsListValid(rows: ClaimRow[], reserved: string[] = []) {\n  return rows.every((row) => claimRowError(row, reserved) === null)\n}\n\nfunction splitValues(text: string) {\n  return text\n    .split(\",\")\n    .map((value) => value.trim())\n    .filter(Boolean)\n}\n\n/** Compile side: fold rows into a claims map, never shadowing reserved names. */\nfunction mergeClaimRows(\n  claims: Record<string, string[]>,\n  rows: ClaimRow[],\n  reserved: string[]\n) {\n  for (const row of rows) {\n    const name = row.name.trim()\n    const values = splitValues(row.values)\n    if (!name || values.length === 0) {\n      continue\n    }\n    if (reserved.includes(name)) {\n      continue\n    }\n    claims[name] = values\n  }\n}\n\n/**\n * Parse side: a row for every claim the form has no dedicated field for, or\n * null when rows cannot faithfully represent them (comma-bearing values,\n * names the row editor rejects) — recompiling would alter the policy.\n */\nfunction claimRowsFromPolicy(\n  claims: Record<string, string[]>,\n  reserved: string[]\n) {\n  const rows: ClaimRow[] = []\n  for (const [name, values] of Object.entries(claims)) {\n    if (reserved.includes(name)) {\n      continue\n    }\n    const row = { name, values: values.join(\", \") }\n    const split = splitValues(row.values)\n    if (\n      claimRowError(row) !== null ||\n      split.length !== values.length ||\n      split.some((value, index) => value !== values[index])\n    ) {\n      return null\n    }\n    rows.push(row)\n  }\n  return rows\n}\n\nfunction ClaimChip({ className, ...props }: React.ComponentProps<\"code\">) {\n  return (\n    <code\n      data-slot=\"claim-chip\"\n      className={cn(\n        \"ml-auto font-mono text-xs font-normal text-muted-foreground\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction ClaimsList({\n  value,\n  onChange,\n  reserved = [],\n  label = \"Additional claims\",\n  className,\n}: {\n  value: ClaimRow[]\n  onChange: (rows: ClaimRow[]) => void\n  /** Claims with dedicated fields — rows may not shadow them. */\n  reserved?: string[]\n  label?: string\n  className?: string\n}) {\n  // Index keys would re-key the rows below a removal and drop focus. Replace\n  // the value from outside by remounting (key the component).\n  const [ids, setIds] = React.useState<string[]>(() =>\n    value.map((_, index) => `seed-${index}`)\n  )\n  const counterRef = React.useRef(0)\n\n  const update = (index: number, patch: Partial<ClaimRow>) =>\n    onChange(value.map((row, i) => (i === index ? { ...row, ...patch } : row)))\n\n  const remove = (index: number) => {\n    setIds(ids.filter((_, i) => i !== index))\n    onChange(value.filter((_, i) => i !== index))\n  }\n\n  const add = () => {\n    setIds([...ids, `row-${counterRef.current++}`])\n    onChange([...value, { name: \"\", values: \"\" }])\n  }\n\n  return (\n    <div\n      data-slot=\"claims-list\"\n      className={cn(\"flex flex-col gap-2\", className)}\n    >\n      <FieldLabel>{label}</FieldLabel>\n      {value.map((row, index) => {\n        const rowError = claimRowError(row, reserved)\n        return (\n          <div key={ids[index] ?? index} className=\"flex flex-col gap-1\">\n            <div className=\"flex items-center gap-2\">\n              <Input\n                value={row.name}\n                placeholder=\"claim\"\n                aria-label=\"Claim name\"\n                className=\"w-40 shrink-0 font-mono text-xs\"\n                onChange={(event) =>\n                  update(index, { name: event.target.value })\n                }\n              />\n              <Input\n                value={row.values}\n                placeholder=\"value1, value2\"\n                aria-label=\"Accepted values\"\n                className=\"font-mono text-xs\"\n                onChange={(event) =>\n                  update(index, { values: event.target.value })\n                }\n              />\n              <Button\n                type=\"button\"\n                variant=\"ghost\"\n                size=\"icon-sm\"\n                aria-label=\"Remove claim\"\n                onClick={() => remove(index)}\n              >\n                <Trash2Icon />\n              </Button>\n            </div>\n            {rowError ? <FieldError>{rowError}</FieldError> : null}\n          </div>\n        )\n      })}\n      <Button\n        type=\"button\"\n        variant=\"outline\"\n        size=\"sm\"\n        className=\"self-start\"\n        onClick={add}\n      >\n        <PlusIcon data-icon=\"inline-start\" />\n        Add claim\n      </Button>\n    </div>\n  )\n}\n\nexport {\n  ClaimsList,\n  ClaimChip,\n  claimRowError,\n  claimsListValid,\n  mergeClaimRows,\n  claimRowsFromPolicy,\n}\nexport type { ClaimRow }\n",
      "type": "registry:component",
      "target": "@components/oidc/claims.tsx"
    }
  ],
  "categories": [
    "auth",
    "forms"
  ],
  "type": "registry:component"
}