{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "access-pass",
  "type": "registry:component",
  "title": "Access Pass",
  "description": "When an agent wants more access it hands you a boarding pass that says who, what and why; pick once, an hour or always, approve, and the stub tears off along the perforation into a little pass with a countdown and Revoke.",
  "categories": [
    "component",
    "Trust & Control"
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "https://coss.com/ui/r/button.json",
    "https://coss.com/ui/r/segmented-control.json"
  ],
  "files": [
    {
      "path": "registry/devl/access-pass.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { InfinityIcon, KeyRoundIcon, TicketCheckIcon, TicketXIcon } from \"lucide-react\";\nimport { AnimatePresence, MotionConfig, motion, useReducedMotion } from \"motion/react\";\nimport { type CSSProperties, useEffect, useId, useRef, useState } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { SegmentedControl, SegmentedControlItem } from \"@/components/ui/segmented-control\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface AccessPassField {\n  label: string;\n  value: string;\n}\n\nexport interface AccessPassRequest {\n  number?: string;\n  agent: { name: string; code?: string; role?: string };\n  resource: { name: string; code?: string };\n  scope: string;\n  reason: string;\n  fields?: AccessPassField[];\n  risk?: \"low\" | \"medium\" | \"high\";\n}\n\nexport interface AccessPassDuration {\n  id: string;\n  label: string;\n  seconds?: number | null;\n  hint: string;\n}\n\nexport interface AccessPassProps {\n  request: AccessPassRequest;\n  durations?: AccessPassDuration[];\n  defaultDuration?: string;\n  onApprove?: (duration: AccessPassDuration) => void;\n  onDeny?: () => void;\n  onRevoke?: () => void;\n  className?: string;\n}\n\ntype Phase = \"pending\" | \"tearing\" | \"approved\" | \"denied\" | \"ended\";\n\nconst defaultDurations: AccessPassDuration[] = [\n  { id: \"once\", label: \"Once\", hint: \"Just this one time, then it asks again.\" },\n  { id: \"hour\", label: \"1 hour\", seconds: 3600, hint: \"For the next hour. Revoke any time.\" },\n  { id: \"always\", label: \"Always\", seconds: null, hint: \"Until you revoke it from settings.\" },\n];\n\nconst risks = {\n  low: { label: \"Low\", className: \"text-success-foreground\" },\n  medium: { label: \"Medium\", className: \"text-warning-foreground\" },\n  high: { label: \"High\", className: \"text-destructive-foreground\" },\n};\n\nconst ease = [0.23, 1, 0.32, 1] as const;\nconst spring = { type: \"spring\", stiffness: 380, damping: 36 } as const;\nconst outline: CSSProperties = {\n  filter:\n    \"drop-shadow(0 0 0.5px color-mix(in oklab, var(--color-foreground) 26%, transparent)) drop-shadow(0 1px 2px color-mix(in oklab, var(--color-foreground) 6%, transparent))\",\n};\nconst NOTCH = 11;\n\nconst notch = (x: string, y: string) => `radial-gradient(circle at ${x} ${y}, transparent ${NOTCH}px, black ${NOTCH + 0.5}px)`;\nconst bodyMask: CSSProperties = {\n  maskImage: `${notch(\"0\", \"100%\")}, ${notch(\"100%\", \"100%\")}`,\n  maskComposite: \"intersect\",\n};\nfunction stubMask(torn: boolean): CSSProperties {\n  if (!torn) return { maskImage: `${notch(\"0\", \"0\")}, ${notch(\"100%\", \"0\")}`, maskComposite: \"intersect\" };\n  return {\n    mask: `${notch(\"0\", \"0\")}, ${notch(\"100%\", \"0\")}, linear-gradient(black, black) bottom / 100% calc(100% - 4px) no-repeat, conic-gradient(from 135deg at 50% 0, black 90deg, transparent 0) top / 8px 4px repeat-x`,\n    maskComposite: \"intersect, intersect, add, add\",\n  } as CSSProperties;\n}\n\nfunction codeOf(name: string) {\n  const letters = name.replace(/[^a-z]/gi, \"\").toUpperCase();\n  const rest = letters.slice(1).replace(/[AEIOU]/g, \"\");\n  return (letters[0] + rest + letters.slice(1)).slice(0, 3);\n}\n\nfunction clock(s: number, total: number) {\n  const pad = (n: number) => String(n).padStart(2, \"0\");\n  if (total <= 3600) return `${pad(Math.floor(s / 60))}:${pad(s % 60)}`;\n  return `${Math.floor(s / 3600)}:${pad(Math.floor(s / 60) % 60)}:${pad(s % 60)}`;\n}\n\nconst labelClass = \"text-[11px] text-muted-foreground uppercase tracking-[0.12em]\";\n\nfunction Label({ children, className }: { children: React.ReactNode; className?: string }) {\n  return <span className={cn(labelClass, className)}>{children}</span>;\n}\n\nexport function AccessPass({\n  request,\n  durations = defaultDurations,\n  defaultDuration = durations[1]?.id ?? durations[0]?.id,\n  onApprove,\n  onDeny,\n  onRevoke,\n  className,\n}: AccessPassProps) {\n  const reduce = useReducedMotion();\n  const labelId = useId();\n  const [phase, setPhase] = useState<Phase>(\"pending\");\n  const [choice, setChoice] = useState(defaultDuration);\n  const [left, setLeft] = useState(0);\n  const [ended, setEnded] = useState<\"revoked\" | \"expired\" | \"used\">(\"revoked\");\n  const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n  const duration = durations.find((d) => d.id === choice) ?? durations[0];\n  const total = duration.seconds ?? 0;\n  const agentCode = request.agent.code ?? codeOf(request.agent.name);\n  const resourceCode = request.resource.code ?? codeOf(request.resource.name);\n  const risk = request.risk ? risks[request.risk] : null;\n\n  useEffect(() => () => clearTimeout(timer.current), []);\n\n  useEffect(() => {\n    if (phase !== \"approved\" || !duration.seconds) return;\n    const t = setInterval(() => {\n      setLeft((s) => {\n        if (s <= 1) {\n          clearInterval(t);\n          setEnded(\"expired\");\n          setPhase(\"ended\");\n          return 0;\n        }\n        return s - 1;\n      });\n    }, 1000);\n    return () => clearInterval(t);\n  }, [phase, duration.seconds]);\n\n  const approve = () => {\n    if (phase !== \"pending\") return;\n    setLeft(total);\n    onApprove?.(duration);\n    if (reduce) return setPhase(\"approved\");\n    setPhase(\"tearing\");\n    timer.current = setTimeout(() => setPhase(\"approved\"), 360);\n  };\n  const deny = () => {\n    setPhase(\"denied\");\n    onDeny?.();\n  };\n  const revoke = () => {\n    setEnded(\"revoked\");\n    setPhase(\"ended\");\n    onRevoke?.();\n  };\n\n  const torn = phase === \"tearing\" || phase === \"approved\" || phase === \"ended\";\n  const collapsed = phase === \"approved\" || phase === \"ended\";\n\n  return (\n    <MotionConfig reducedMotion=\"user\" transition={spring}>\n      <section aria-labelledby={labelId} className={cn(\"flex w-[380px] max-w-full flex-col\", className)}>\n        <AnimatePresence initial={false}>\n          {!collapsed && (\n            <motion.div\n              animate={{ height: \"auto\", opacity: 1 }}\n              className=\"-mx-1 -mt-1 flex flex-col justify-end overflow-hidden px-1 pt-1\"\n              exit={{ height: 0, opacity: 0, transition: { height: spring, opacity: { duration: 0.18, ease } } }}\n              initial={{ height: 0, opacity: 0 }}\n              key=\"body\"\n            >\n              <div style={outline}>\n                <div className=\"relative flex flex-col gap-5 bg-card px-5 pt-5 pb-6\" style={bodyMask}>\n                  <div className={cn(\"flex flex-col gap-5 transition-opacity duration-200\", phase === \"denied\" && \"opacity-45\")}>\n                    <header className=\"flex items-center gap-3\">\n                      <span className=\"flex size-9 shrink-0 items-center justify-center rounded-lg bg-violet-500/12 font-medium font-mono text-sm text-violet-700 dark:bg-violet-400/16 dark:text-violet-300\">\n                        {request.agent.name[0]}\n                      </span>\n                      <div className=\"flex min-w-0 flex-1 flex-col\">\n                        <h3 className=\"truncate font-medium text-sm\" id={labelId}>\n                          {request.agent.name} wants access\n                        </h3>\n                        <span className=\"truncate text-muted-foreground text-xs\">{request.scope}</span>\n                      </div>\n                      <div className=\"flex shrink-0 flex-col items-end\">\n                        <Label>Access pass</Label>\n                        {request.number && <span className=\"font-mono text-muted-foreground text-xs tabular-nums\">{request.number}</span>}\n                      </div>\n                    </header>\n\n                    <div className=\"flex items-center gap-3\">\n                      <div className=\"flex min-w-0 flex-col\">\n                        <span className=\"font-mono font-semibold text-3xl leading-none tracking-tight\">{agentCode}</span>\n                        <span className=\"mt-1.5 truncate text-muted-foreground text-xs\">{request.agent.role ?? \"Agent\"}</span>\n                      </div>\n                      <div aria-hidden className=\"relative flex min-w-8 flex-1 items-center self-start pt-3\">\n                        <span className=\"h-px flex-1 border-foreground/25 border-t border-dashed\" />\n                        <span className=\"mx-1.5 flex size-6 items-center justify-center rounded-full border bg-background text-muted-foreground\">\n                          <KeyRoundIcon className=\"size-3\" />\n                        </span>\n                        <span className=\"h-px flex-1 border-foreground/25 border-t border-dashed\" />\n                      </div>\n                      <div className=\"flex min-w-0 flex-col items-end\">\n                        <span className=\"font-mono font-semibold text-3xl leading-none tracking-tight\">{resourceCode}</span>\n                        <span className=\"mt-1.5 max-w-36 truncate font-mono text-muted-foreground text-xs\">{request.resource.name}</span>\n                      </div>\n                    </div>\n\n                    {Boolean(request.fields?.length || risk) && (\n                      <dl className=\"grid grid-cols-3 gap-3\">\n                        {request.fields?.slice(0, risk ? 2 : 3).map((f) => (\n                          <div className=\"flex min-w-0 flex-col gap-1\" key={f.label}>\n                            <dt className={cn(labelClass, \"truncate\")}>{f.label}</dt>\n                            <dd className=\"truncate font-medium font-mono text-xs\">{f.value}</dd>\n                          </div>\n                        ))}\n                        {risk && (\n                          <div className=\"flex min-w-0 flex-col gap-1\">\n                            <dt className={labelClass}>Risk</dt>\n                            <dd className={cn(\"flex items-center gap-1.5 font-medium font-mono text-xs\", risk.className)}>\n                              <span className=\"size-1.5 rounded-full bg-current\" />\n                              {risk.label}\n                            </dd>\n                          </div>\n                        )}\n                      </dl>\n                    )}\n\n                    <div className=\"flex flex-col gap-1.5 rounded-xl bg-muted/70 px-3.5 py-3\">\n                      <Label>Why</Label>\n                      <p className=\"text-pretty text-sm\">{request.reason}</p>\n                    </div>\n                  </div>\n\n                  <AnimatePresence>\n                    {phase === \"denied\" && (\n                      <motion.span\n                        animate={{ opacity: 1, scale: 1, rotate: -9 }}\n                        aria-hidden\n                        className=\"pointer-events-none absolute top-[42%] left-1/2 -ml-[72px] w-36 rounded-lg border-[3px] border-destructive/75 py-1 pl-[0.3em] text-center font-bold font-mono text-3xl text-destructive-foreground/90 tracking-[0.3em]\"\n                        exit={{ opacity: 0, transition: { duration: 0.15 } }}\n                        initial={reduce ? { opacity: 0, rotate: -9 } : { opacity: 0, scale: 1.45, rotate: -15 }}\n                        transition={{ type: \"spring\", stiffness: 700, damping: 34 }}\n                      >\n                        VOID\n                      </motion.span>\n                    )}\n                  </AnimatePresence>\n                </div>\n              </div>\n            </motion.div>\n          )}\n        </AnimatePresence>\n\n        <motion.div\n          animate={phase === \"tearing\" ? { y: 12, rotate: 2.2, x: 4 } : { y: 0, rotate: 0, x: 0 }}\n          className=\"relative z-10\"\n          style={{ transformOrigin: \"0% 0%\", ...outline }}\n          transition={phase === \"tearing\" ? { duration: 0.3, ease } : spring}\n        >\n          <motion.div\n            animate={{ height: collapsed ? 40 : \"auto\", borderRadius: collapsed ? 20 : 0 }}\n            className=\"relative overflow-hidden bg-card\"\n            initial={false}\n            style={collapsed ? undefined : stubMask(torn)}\n          >\n            {!torn && (\n              <span aria-hidden className=\"pointer-events-none absolute inset-x-4 top-0 border-foreground/25 border-t-2 border-dashed\" />\n            )}\n            <AnimatePresence initial={false} mode=\"popLayout\">\n              {collapsed ? (\n                <motion.div\n                  animate={{ opacity: 1 }}\n                  className=\"max-w-full\"\n                  exit={{ opacity: 0, transition: { duration: 0.1 } }}\n                  initial={{ opacity: 0 }}\n                  key={`chip-${phase}`}\n                  transition={{ opacity: { duration: 0.2, delay: 0.08, ease } }}\n                >\n                  {phase === \"approved\" ? (\n                    <PassChip\n                      agent={request.agent.name}\n                      left={left}\n                      onRevoke={revoke}\n                      scope={request.scope}\n                      seconds={duration.seconds}\n                      total={total}\n                    />\n                  ) : (\n                    <div className=\"flex h-10 items-center gap-2.5 pr-1.5 pl-3 text-muted-foreground text-sm\" role=\"status\">\n                      <TicketXIcon className=\"size-4 shrink-0\" />\n                      <span className=\"min-w-0 flex-1 truncate\">\n                        {ended === \"expired\" ? \"Pass expired\" : \"Pass revoked\"}\n                        <span> · {request.agent.name} will ask next time</span>\n                      </span>\n                      <Button className=\"rounded-full\" onClick={() => setPhase(\"pending\")} size=\"xs\" variant=\"ghost\">\n                        Reissue\n                      </Button>\n                    </div>\n                  )}\n                </motion.div>\n              ) : (\n                <motion.div\n                  animate={{ opacity: 1 }}\n                  className={cn(\"flex flex-col gap-3 px-5 pt-5 pb-5\", phase === \"tearing\" && \"pointer-events-none\")}\n                  exit={{ opacity: 0, transition: { duration: 0.08 } }}\n                  initial={{ opacity: 0 }}\n                  key=\"stub\"\n                >\n                  {phase === \"denied\" ? (\n                    <div className=\"flex items-center gap-3\" role=\"status\">\n                      <span className=\"flex-1 text-muted-foreground text-sm\">\n                        Denied. {request.agent.name} won’t {request.scope.charAt(0).toLowerCase() + request.scope.slice(1)}.\n                      </span>\n                      <Button onClick={() => setPhase(\"pending\")} size=\"sm\" variant=\"outline\">\n                        Undo\n                      </Button>\n                    </div>\n                  ) : (\n                    <>\n                      <div className=\"flex items-center justify-between gap-3\">\n                        <Label>Valid for</Label>\n                        <span className=\"truncate text-muted-foreground text-xs\">{duration.hint}</span>\n                      </div>\n                      <SegmentedControl aria-label=\"How long the pass lasts\" className=\"w-full\" role=\"radiogroup\">\n                        {durations.map((d) => (\n                          <SegmentedControlItem\n                            aria-checked={d.id === choice}\n                            className=\"flex-1 text-muted-foreground hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring\"\n                            data-checked={d.id === choice ? \"\" : undefined}\n                            key={d.id}\n                            onClick={() => setChoice(d.id)}\n                            role=\"radio\"\n                          >\n                            {d.label}\n                          </SegmentedControlItem>\n                        ))}\n                      </SegmentedControl>\n                      <div className=\"flex gap-2\">\n                        <Button className=\"flex-1\" onClick={deny} variant=\"outline\">\n                          Deny\n                        </Button>\n                        <Button className=\"flex-[2] transition-[scale,box-shadow] duration-150 ease-out active:scale-[0.97]\" onClick={approve}>\n                          <TicketCheckIcon />\n                          Approve {duration.label.toLowerCase()}\n                        </Button>\n                      </div>\n                    </>\n                  )}\n                </motion.div>\n              )}\n            </AnimatePresence>\n          </motion.div>\n        </motion.div>\n      </section>\n    </MotionConfig>\n  );\n}\n\nfunction PassChip({\n  agent,\n  scope,\n  seconds,\n  left,\n  total,\n  onRevoke,\n}: {\n  agent: string;\n  scope: string;\n  seconds?: number | null;\n  left: number;\n  total: number;\n  onRevoke: () => void;\n}) {\n  const r = 9;\n  const c = 2 * Math.PI * r;\n  const share = seconds ? left / Math.max(total, 1) : 1;\n  return (\n    <div className=\"flex h-10 max-w-full items-center gap-2.5 pr-1.5 pl-1.5\" role=\"status\">\n      <span className=\"relative flex size-7 shrink-0 items-center justify-center rounded-full bg-success/12 text-success-foreground\">\n        <svg aria-hidden className=\"absolute inset-0 -rotate-90\" viewBox=\"0 0 28 28\">\n          <circle className=\"stroke-success/20\" cx=\"14\" cy=\"14\" fill=\"none\" r={r} strokeWidth=\"2\" />\n          <circle\n            className=\"stroke-success transition-[stroke-dashoffset] duration-1000 ease-linear\"\n            cx=\"14\"\n            cy=\"14\"\n            fill=\"none\"\n            r={r}\n            strokeDasharray={c}\n            strokeDashoffset={c * (1 - share)}\n            strokeLinecap=\"round\"\n            strokeWidth=\"2\"\n          />\n        </svg>\n        <KeyRoundIcon className=\"relative size-3\" />\n      </span>\n      <span className=\"min-w-0 flex-1 truncate text-sm\">\n        <span className=\"font-medium\">{agent}</span>\n        <span className=\"text-muted-foreground\"> · {scope}</span>\n      </span>\n      <span className=\"flex shrink-0 items-center gap-1 font-mono text-muted-foreground text-xs tabular-nums\">\n        {seconds === undefined ? \"1 use\" : seconds === null ? <InfinityIcon aria-label=\"No expiry\" className=\"size-3.5\" /> : clock(left, total)}\n      </span>\n      <Button className=\"shrink-0 rounded-full\" onClick={onRevoke} size=\"xs\" variant=\"ghost\">\n        Revoke\n      </Button>\n    </div>\n  );\n}\n"
    }
  ]
}