{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "provider-vercel-rhf",
  "title": "Vercel Provider (React Hook Form)",
  "description": "A self-contained form component that emits an OidcPolicy for a Vercel caller: team, project, and environment scoping with derived issuer, plus custom audiences via Vercel's OIDC token exchange. Built with React Hook Form.",
  "dependencies": [
    "react-hook-form"
  ],
  "registryDependencies": [
    "https://ui.shadcn.com/oidc/r/policy.json",
    "https://ui.shadcn.com/oidc/r/claims.json",
    "button",
    "field",
    "input",
    "alert"
  ],
  "files": [
    {
      "path": "registry/oidc/flavors/rhf-none/provider-vercel.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Controller, useForm, useWatch } from \"react-hook-form\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\nimport { Field, FieldDescription, FieldLabel } from \"@/components/ui/field\"\nimport { Input } from \"@/components/ui/input\"\nimport { Alert, AlertTitle } from \"@/components/ui/alert\"\nimport {\n  ClaimChip,\n  ClaimsList,\n  claimRowsFromPolicy,\n  claimsListValid,\n  mergeClaimRows,\n  type ClaimRow,\n} from \"@/components/oidc/claims\"\nimport type { OidcPolicy } from \"@/lib/oidc/policy\"\n\nfunction VercelIcon({ ...props }: React.ComponentProps<\"svg\">) {\n  return (\n    <svg viewBox=\"0 0 24 24\" fill=\"currentColor\" {...props}>\n      <path d=\"m12 1.608 12 20.784H0Z\" />\n    </svg>\n  )\n}\n\ntype Fields = {\n  teamSlug: string\n  ownerId: string\n  projectId: string\n  environment: string\n  audience: string\n  extraClaims: ClaimRow[]\n}\n\nconst DEFAULTS: Fields = {\n  teamSlug: \"\",\n  ownerId: \"\",\n  projectId: \"\",\n  environment: \"\",\n  audience: \"\",\n  extraClaims: [],\n}\n\nconst FIRST_CLASS_CLAIMS = [\"aud\", \"owner_id\", \"project_id\", \"environment\"]\n\nconst GLOBAL_ISSUER = \"https://oidc.vercel.com\"\n\nfunction defaultAudience(teamSlug: string) {\n  return teamSlug ? `https://vercel.com/${teamSlug}` : \"\"\n}\n\nfunction compile(fields: Fields) {\n  const claims: Record<string, string[]> = {}\n  if (fields.audience) {\n    claims.aud = [fields.audience]\n  }\n  if (fields.ownerId) {\n    claims.owner_id = [fields.ownerId]\n  }\n  if (fields.projectId) {\n    claims.project_id = [fields.projectId]\n  }\n  if (fields.environment) {\n    claims.environment = [fields.environment]\n  }\n  mergeClaimRows(claims, fields.extraClaims, FIRST_CLASS_CLAIMS)\n  return {\n    issuer: fields.teamSlug ? `${GLOBAL_ISSUER}/${fields.teamSlug}` : \"\",\n    claims,\n  }\n}\n\nfunction parse(policy: OidcPolicy) {\n  if (!policy.issuer.startsWith(`${GLOBAL_ISSUER}/`)) {\n    return null\n  }\n  const teamSlug = policy.issuer.slice(`${GLOBAL_ISSUER}/`.length)\n  if (!/^[\\w-]+$/.test(teamSlug)) {\n    return null\n  }\n\n  if (\n    FIRST_CLASS_CLAIMS.some((name) => (policy.claims[name]?.length ?? 0) > 1)\n  ) {\n    return null\n  }\n  const extraClaims = claimRowsFromPolicy(policy.claims, FIRST_CLASS_CLAIMS)\n  if (extraClaims === null) {\n    return null\n  }\n\n  const [aud = \"\"] = policy.claims.aud ?? []\n\n  return {\n    teamSlug,\n    ownerId: policy.claims.owner_id?.[0] ?? \"\",\n    projectId: policy.claims.project_id?.[0] ?? \"\",\n    environment: policy.claims.environment?.[0] ?? \"\",\n    audience: aud,\n    extraClaims,\n  }\n}\n\n/** True when this form can represent the policy — for routing an edit. */\nfunction matchesVercelPolicy(policy: OidcPolicy) {\n  return parse(policy) !== null\n}\n\nfunction VercelPolicyForm({\n  defaultValue,\n  onChange,\n  onSubmit,\n  className,\n}: {\n  defaultValue?: OidcPolicy\n  onChange?: (policy: OidcPolicy) => void\n  onSubmit: (policy: OidcPolicy) => void\n  className?: string\n}) {\n  const form = useForm<Fields>({\n    mode: \"onChange\",\n    defaultValues: defaultValue ? (parse(defaultValue) ?? DEFAULTS) : DEFAULTS,\n  })\n  const watched = {\n    teamSlug: useWatch({ control: form.control, name: \"teamSlug\" }),\n    environment: useWatch({ control: form.control, name: \"environment\" }),\n  }\n\n  const onChangeRef = React.useRef(onChange)\n  React.useEffect(() => {\n    onChangeRef.current = onChange\n  }, [onChange])\n\n  React.useEffect(() => {\n    return form.subscribe({\n      formState: { values: true },\n      callback: ({ values }) => {\n        const policy = compile(values)\n        onChangeRef.current?.(\n          defaultValue?.label\n            ? { ...policy, label: defaultValue.label }\n            : policy\n        )\n      },\n    })\n  }, [form, defaultValue?.label])\n\n  return (\n    <form\n      data-slot=\"vercel-policy-form\"\n      className={cn(\"flex flex-col gap-4\", className)}\n      onSubmit={form.handleSubmit((fields) => {\n        if (!claimsListValid(fields.extraClaims, FIRST_CLASS_CLAIMS)) {\n          return\n        }\n        const policy = compile(fields)\n        onSubmit(\n          defaultValue?.label\n            ? { ...policy, label: defaultValue.label }\n            : policy\n        )\n      })}\n    >\n      <Controller\n        control={form.control}\n        name=\"teamSlug\"\n        rules={{ required: true }}\n        render={({ field }) => (\n          <Field>\n            <FieldLabel htmlFor=\"vc-teamSlug\" className=\"w-full\">\n              Team slug <ClaimChip>iss</ClaimChip>\n            </FieldLabel>\n            <Input\n              {...field}\n              id=\"vc-teamSlug\"\n              placeholder=\"acme\"\n              onChange={(event) => field.onChange(event.target.value.trim())}\n            />\n            <FieldDescription>\n              The team URL segment: vercel.com/acme.\n            </FieldDescription>\n          </Field>\n        )}\n      />\n      <Controller\n        control={form.control}\n        name=\"ownerId\"\n        rules={{ required: true }}\n        render={({ field }) => (\n          <Field>\n            <FieldLabel htmlFor=\"vc-ownerId\" className=\"w-full\">\n              Team ID <ClaimChip>owner_id</ClaimChip>\n            </FieldLabel>\n            <Input\n              {...field}\n              id=\"vc-ownerId\"\n              placeholder=\"team_xxxxxxxx\"\n              onChange={(event) => field.onChange(event.target.value.trim())}\n            />\n            <FieldDescription>\n              Team settings → General → Team ID.\n            </FieldDescription>\n          </Field>\n        )}\n      />\n      <Controller\n        control={form.control}\n        name=\"projectId\"\n        render={({ field }) => (\n          <Field>\n            <FieldLabel htmlFor=\"vc-projectId\" className=\"w-full\">\n              Project ID <ClaimChip>project_id</ClaimChip>\n            </FieldLabel>\n            <Input\n              {...field}\n              id=\"vc-projectId\"\n              placeholder=\"prj_xxxxxxxx\"\n              onChange={(event) => field.onChange(event.target.value.trim())}\n            />\n            <FieldDescription>\n              Project settings → General → Project ID. Leave empty to trust the\n              whole team.\n            </FieldDescription>\n          </Field>\n        )}\n      />\n      <Controller\n        control={form.control}\n        name=\"environment\"\n        render={({ field }) => (\n          <Field>\n            <FieldLabel htmlFor=\"vc-environment\" className=\"w-full\">\n              Environment <ClaimChip>environment</ClaimChip>\n            </FieldLabel>\n            <Input\n              {...field}\n              id=\"vc-environment\"\n              placeholder=\"production\"\n              onChange={(event) => field.onChange(event.target.value.trim())}\n            />\n            <FieldDescription>\n              Optional: production, preview, or development.\n            </FieldDescription>\n          </Field>\n        )}\n      />\n      <Controller\n        control={form.control}\n        name=\"audience\"\n        rules={{ required: true }}\n        render={({ field }) => (\n          <Field>\n            <FieldLabel htmlFor=\"vc-audience\" className=\"w-full\">\n              Audience <ClaimChip>aud</ClaimChip>\n            </FieldLabel>\n            <Input\n              {...field}\n              id=\"vc-audience\"\n              placeholder={\n                defaultAudience(watched.teamSlug) || \"https://vercel.com/acme\"\n              }\n              onChange={(event) => field.onChange(event.target.value.trim())}\n            />\n            <FieldDescription>\n              The team URL is the default tokens carry; deployments can request\n              a custom audience via Vercel&apos;s token exchange.\n            </FieldDescription>\n          </Field>\n        )}\n      />\n      {watched.teamSlug ? (\n        <FieldDescription>\n          Issuer derived automatically:{\" \"}\n          <span className=\"font-mono\">\n            {GLOBAL_ISSUER}/{watched.teamSlug}\n          </span>\n        </FieldDescription>\n      ) : null}\n      {watched.teamSlug && !watched.environment ? (\n        <Alert>\n          <AlertTitle>\n            Without an environment, preview deployments get the same access as\n            production.\n          </AlertTitle>\n        </Alert>\n      ) : null}\n      <Controller\n        control={form.control}\n        name=\"extraClaims\"\n        render={({ field: controller }) => (\n          <ClaimsList\n            value={controller.value}\n            onChange={controller.onChange}\n            reserved={FIRST_CLASS_CLAIMS}\n          />\n        )}\n      />\n      <Button type=\"submit\" className=\"self-start\">\n        Save\n      </Button>\n    </form>\n  )\n}\n\nexport { VercelIcon, VercelPolicyForm, matchesVercelPolicy }\n",
      "type": "registry:component",
      "target": "@components/oidc/provider-vercel.tsx"
    }
  ],
  "meta": {
    "tagline": "Trust deployments from a team or project"
  },
  "categories": [
    "auth",
    "forms"
  ],
  "type": "registry:component"
}