{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "feature-gate",
  "title": "Feature Gate",
  "description": "Locks a feature outside the plan with a plan badge, preview and upgrade action.",
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "@ikas/badge",
    "@ikas/button",
    "@ikas/motion",
    "@ikas/popover",
    "@ikas/utils"
  ],
  "files": [
    {
      "path": "registry/ikas/feature-gate.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { AnimatePresence, motion, useAnimate } from \"motion/react\"\nimport { CheckIcon, LockKeyholeIcon, LockKeyholeOpenIcon } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { EASE_OUT, FADE_IN, FADE_OUT, ICON_SPRING, INSTANT, usePrefersReducedMotion } from \"@/lib/motion\"\nimport { Badge, type BadgeColor } from \"@/components/ui/badge\"\nimport { Button } from \"@/components/ui/button\"\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/components/ui/popover\"\n\ntype FeatureGateVariant = \"overlay\" | \"preview\" | \"banner\" | \"inline\" | \"action\"\ntype FeatureGateColor = BadgeColor\n\ntype FeatureGateProps = {\n  /** True while the plan does not include the feature. False renders `children` as they are. */\n  locked: boolean\n  /**\n   * overlay blurs content the user already has; preview shows sample content when there is none yet;\n   * banner keeps a section readable but read-only; inline swaps one control in a list; action wraps a single button.\n   */\n  variant?: FeatureGateVariant\n  /** Feature name, next to the lock. */\n  title: React.ReactNode\n  /** One sentence on what the plan unlocks. */\n  description?: React.ReactNode\n  /** Plan that includes the feature, shown as a badge. */\n  plan?: React.ReactNode\n  /** Palette color of the lock, badge and upgrade button. */\n  color?: FeatureGateColor\n  /** Upgrade button text. Defaults to \"Planı yükselt\", or \"Yükselt\" in the compact variants. */\n  upgradeLabel?: React.ReactNode\n  /** Opens the plans or checkout. */\n  onUpgrade?: () => void\n  /** Secondary link text. */\n  compareLabel?: React.ReactNode\n  /** Shows the secondary link as a button. */\n  onCompare?: () => void\n  /** Shows the secondary link as an anchor. */\n  compareHref?: string\n  /** Sample content for variant=\"preview\" while there is no real data. Defaults to `children`. */\n  preview?: React.ReactNode\n  /** The gated content. For variant=\"action\", a single button. */\n  children?: React.ReactNode\n  className?: string\n}\n\n/** The text color the lock takes, and the soft fill the banner strip sits on. */\nconst tints = {\n  neutral: \"[--gate:var(--neutral-subtle-foreground)] [--gate-subtle:var(--neutral-subtle)] [--gate-border:var(--neutral-border)]\",\n  blue: \"[--gate:var(--blue-subtle-foreground)] [--gate-subtle:var(--blue-subtle)] [--gate-border:var(--blue-border)]\",\n  violet: \"[--gate:var(--violet-subtle-foreground)] [--gate-subtle:var(--violet-subtle)] [--gate-border:var(--violet-border)]\",\n  green: \"[--gate:var(--green-subtle-foreground)] [--gate-subtle:var(--green-subtle)] [--gate-border:var(--green-border)]\",\n  lime: \"[--gate:var(--lime-subtle-foreground)] [--gate-subtle:var(--lime-subtle)] [--gate-border:var(--lime-border)]\",\n  amber: \"[--gate:var(--amber-subtle-foreground)] [--gate-subtle:var(--amber-subtle)] [--gate-border:var(--amber-border)]\",\n  red: \"[--gate:var(--red-subtle-foreground)] [--gate-subtle:var(--red-subtle)] [--gate-border:var(--red-border)]\",\n} satisfies Record<FeatureGateColor, string>\n\ntype Phase = \"locked\" | \"opening\" | \"open\"\n\n/** How long each unlock beat holds before the next one: the shackle lifts, then the check lands, then the content returns. */\nconst OPEN_MS = 260\nconst DONE_MS = 560\n\n/**\n * Keeps the gate on screen for a short unlock sequence after `locked` turns false.\n * Locking again is immediate, and reduced motion skips the sequence.\n */\nfunction useUnlock(locked: boolean, reduce: boolean) {\n  const [shown, setShown] = React.useState(locked)\n  const [phase, setPhase] = React.useState<Phase>(locked ? \"locked\" : \"open\")\n  const [prev, setPrev] = React.useState(locked)\n\n  if (locked !== prev) {\n    setPrev(locked)\n    if (locked || reduce) {\n      setShown(locked)\n      setPhase(locked ? \"locked\" : \"open\")\n    } else {\n      setPhase(\"opening\")\n    }\n  }\n\n  React.useEffect(() => {\n    if (phase === \"opening\") {\n      const timer = setTimeout(() => setPhase(\"open\"), OPEN_MS)\n      return () => clearTimeout(timer)\n    }\n    if (phase === \"open\" && shown && !locked) {\n      const timer = setTimeout(() => setShown(false), DONE_MS - OPEN_MS)\n      return () => clearTimeout(timer)\n    }\n  }, [phase, shown, locked])\n\n  return { shown, phase }\n}\n\n/** Lock that drops into place once, swings when the gate is hovered and opens into a check on unlock. */\nfunction GateLock({ phase, reduce, swing }: { phase: Phase; reduce: boolean; swing: number }) {\n  const [scope, animate] = useAnimate<HTMLSpanElement>()\n\n  React.useEffect(() => {\n    if (!swing || reduce || !scope.current) return\n    animate(scope.current, { rotate: [0, -9, 7, 0] }, { duration: 0.32, ease: EASE_OUT })\n  }, [swing, reduce, animate, scope])\n\n  const Icon = phase === \"locked\" ? LockKeyholeIcon : phase === \"opening\" ? LockKeyholeOpenIcon : CheckIcon\n\n  return (\n    <motion.span\n      aria-hidden\n      data-slot=\"feature-gate-lock\"\n      className={cn(\"relative grid size-4 shrink-0 place-items-center\", phase === \"open\" ? \"text-success-subtle-foreground\" : \"text-(--gate)\")}\n      initial={reduce ? false : { y: -6, opacity: 0 }}\n      animate={{ y: 0, opacity: 1 }}\n      transition={reduce ? INSTANT : { ...ICON_SPRING, opacity: FADE_IN }}\n    >\n      <span ref={scope} className=\"grid size-4 place-items-center\">\n        <AnimatePresence initial={false} mode=\"popLayout\">\n          <motion.span\n            key={phase}\n            className=\"absolute grid place-items-center\"\n            initial={reduce ? false : { scale: 0.6, opacity: 0 }}\n            animate={{ scale: 1, opacity: 1 }}\n            exit={reduce ? { opacity: 0, transition: INSTANT } : { scale: 0.6, opacity: 0, transition: FADE_OUT }}\n            transition={reduce ? INSTANT : { ...ICON_SPRING, opacity: FADE_IN }}\n          >\n            <Icon className=\"size-4\" />\n          </motion.span>\n        </AnimatePresence>\n      </span>\n    </motion.span>\n  )\n}\n\n/** Plan badge with one light sweep when the gate appears. */\nfunction PlanBadge({ plan, color, reduce }: { plan: React.ReactNode; color: FeatureGateColor; reduce: boolean }) {\n  return (\n    <Badge data-slot=\"feature-gate-plan\" color={color} size=\"sm\" className=\"relative\">\n      {plan}\n      {!reduce && (\n        <motion.span\n          aria-hidden\n          className=\"pointer-events-none absolute inset-0 bg-linear-[105deg] from-transparent from-35% via-card/80 via-50% to-transparent to-65%\"\n          initial={{ x: \"-100%\" }}\n          animate={{ x: \"100%\" }}\n          transition={{ duration: 0.7, ease: EASE_OUT, delay: 0.25 }}\n        />\n      )}\n    </Badge>\n  )\n}\n\ntype PartsProps = Required<Pick<FeatureGateProps, \"plan\" | \"color\" | \"compareLabel\">> &\n  Pick<FeatureGateProps, \"title\" | \"description\" | \"onUpgrade\" | \"onCompare\" | \"compareHref\"> & {\n    upgradeLabel: React.ReactNode\n    phase: Phase\n    reduce: boolean\n    swing: number\n    titleId: string\n  }\n\nfunction Heading({ title, plan, color, phase, reduce, swing, titleId, center }: PartsProps & { center?: boolean }) {\n  return (\n    <div className={cn(\"flex items-center gap-2\", center && \"justify-center\")}>\n      <GateLock phase={phase} reduce={reduce} swing={swing} />\n      <h3 id={titleId} className=\"text-[15px] font-semibold text-foreground\">\n        {title}\n      </h3>\n      <PlanBadge plan={plan} color={color} reduce={reduce} />\n    </div>\n  )\n}\n\n/** The same pair as every other action row: outline for the secondary step, solid for the main one. */\nfunction Actions({ upgradeLabel, onUpgrade, compareLabel, onCompare, compareHref, color, center }: PartsProps & { center?: boolean }) {\n  return (\n    <div className={cn(\"flex flex-wrap items-center gap-2 pt-1\", center && \"justify-center\")}>\n      {compareHref ? (\n        <Button asChild variant=\"outline\">\n          <a href={compareHref}>{compareLabel}</a>\n        </Button>\n      ) : onCompare ? (\n        <Button variant=\"outline\" onClick={onCompare}>\n          {compareLabel}\n        </Button>\n      ) : null}\n      <Button color={color} onClick={onUpgrade}>\n        {upgradeLabel}\n      </Button>\n    </div>\n  )\n}\n\n/** Locks a feature that is outside the user's plan, without hiding that the feature exists. */\nfunction FeatureGate({\n  locked,\n  variant = \"overlay\",\n  title,\n  description,\n  plan = \"Pro\",\n  color = \"violet\",\n  upgradeLabel,\n  onUpgrade,\n  compareLabel = \"Planları karşılaştır\",\n  onCompare,\n  compareHref,\n  preview,\n  children,\n  className,\n}: FeatureGateProps) {\n  const reduce = usePrefersReducedMotion()\n  const sequenced = variant === \"overlay\" || variant === \"preview\"\n  const { shown, phase } = useUnlock(locked, reduce || !sequenced)\n  const [swing, setSwing] = React.useState(0)\n  const titleId = React.useId()\n\n  const parts: PartsProps = {\n    title,\n    description,\n    plan,\n    color,\n    upgradeLabel: upgradeLabel ?? (variant === \"overlay\" || variant === \"action\" ? \"Planı yükselt\" : \"Yükselt\"),\n    onUpgrade,\n    compareLabel,\n    onCompare,\n    compareHref,\n    phase,\n    reduce,\n    swing,\n    titleId,\n  }\n\n  // Swing the lock on a fine pointer only; touch has no hover to answer.\n  const onPointerEnter = (event: React.PointerEvent) => {\n    if (event.pointerType === \"mouse\") setSwing((n) => n + 1)\n  }\n\n  const root = (extra?: string) => ({\n    \"data-slot\": \"feature-gate\",\n    \"data-variant\": variant,\n    \"data-locked\": shown || undefined,\n    className: cn(tints[color], extra, className),\n  })\n\n  if (variant === \"inline\") {\n    return (\n      <span {...root(\"inline-flex items-center\")}>\n        {shown ? (\n          <span className=\"inline-flex items-center gap-2\">\n            <PlanBadge plan={plan} color={color} reduce={reduce} />\n            <Button size=\"sm\" variant=\"outline\" aria-label={typeof title === \"string\" ? `${title}: ${parts.upgradeLabel}` : undefined} onClick={onUpgrade}>\n              {parts.upgradeLabel}\n            </Button>\n          </span>\n        ) : (\n          children\n        )}\n      </span>\n    )\n  }\n\n  if (variant === \"action\") {\n    if (!shown || !React.isValidElement<{ children?: React.ReactNode; onClick?: unknown }>(children)) return <>{children}</>\n    // The button's own action waits for the plan; while locked a click explains the plan instead.\n    const trigger = React.cloneElement(\n      children,\n      { onClick: undefined },\n      children.props.children,\n      <LockKeyholeIcon key=\"feature-gate-lock\" data-icon=\"inline-end\" data-anim=\"wiggle\" className=\"text-(--gate)\" />,\n    )\n    return (\n      <Popover>\n        <PopoverTrigger asChild {...root()}>\n          {trigger}\n        </PopoverTrigger>\n        <PopoverContent align=\"start\" aria-labelledby={titleId} className={cn(tints[color], \"w-80 gap-2.5 p-3.5\")}>\n          <Heading {...parts} />\n          {description && <p className=\"text-[13px] text-muted-foreground\">{description}</p>}\n          <Actions {...parts} />\n        </PopoverContent>\n      </Popover>\n    )\n  }\n\n  if (variant === \"banner\") {\n    return (\n      <div {...root()}>\n        <AnimatePresence initial={false}>\n          {shown && (\n            <motion.div\n              data-slot=\"feature-gate-banner\"\n              role=\"region\"\n              aria-labelledby={titleId}\n              className=\"flex items-center gap-2.5 overflow-hidden border-b border-(--gate-border) bg-(--gate-subtle) px-4 py-2.5\"\n              initial={reduce ? false : { opacity: 0 }}\n              animate={{ opacity: 1 }}\n              exit={reduce ? { opacity: 0, transition: INSTANT } : { opacity: 0, transition: FADE_OUT }}\n            >\n              <LockKeyholeIcon aria-hidden className=\"size-4 shrink-0 text-(--gate)\" />\n              <p className=\"min-w-0 flex-1 text-[13px] text-(--gate)\">\n                <span id={titleId} className=\"font-medium\">\n                  {title}\n                </span>\n                {description && <> · {description}</>}\n              </p>\n              <Button size=\"sm\" color={color} onClick={onUpgrade}>\n                {parts.upgradeLabel}\n              </Button>\n            </motion.div>\n          )}\n        </AnimatePresence>\n        <div data-slot=\"feature-gate-content\" inert={shown} className={cn(shown && \"select-none [&_input]:text-muted-foreground\")}>\n          {children}\n        </div>\n      </div>\n    )\n  }\n\n  if (variant === \"preview\") {\n    return (\n      <div {...root(\"relative\")} onPointerEnter={onPointerEnter}>\n        {shown ? (\n          <div data-slot=\"feature-gate-preview\" inert aria-hidden className=\"pointer-events-none min-h-60 mask-b-from-15% mask-b-to-80% select-none\">\n            {preview ?? children}\n          </div>\n        ) : (\n          children\n        )}\n        <AnimatePresence>\n          {shown && (\n            <motion.div\n              role=\"region\"\n              aria-labelledby={titleId}\n              className=\"absolute inset-x-0 bottom-0 flex flex-wrap items-center gap-x-4 gap-y-3 p-4\"\n              initial={reduce ? false : { opacity: 0, y: 8 }}\n              animate={{ opacity: 1, y: 0 }}\n              exit={reduce ? { opacity: 0, transition: INSTANT } : { opacity: 0, transition: FADE_OUT }}\n              transition={reduce ? INSTANT : { duration: 0.3, ease: EASE_OUT }}\n            >\n              <div className=\"flex min-w-0 flex-1 flex-col gap-1\">\n                <Heading {...parts} />\n                {description && <p className=\"pl-6 text-[13px] text-muted-foreground\">{description}</p>}\n              </div>\n              <Button color={color} onClick={onUpgrade}>\n                {parts.upgradeLabel}\n              </Button>\n            </motion.div>\n          )}\n        </AnimatePresence>\n      </div>\n    )\n  }\n\n  return (\n    <div {...root(\"relative isolate\")} onPointerEnter={onPointerEnter}>\n      <div\n        data-slot=\"feature-gate-content\"\n        inert={shown}\n        aria-hidden={shown || undefined}\n        className={cn(\n          \"transition-[filter,opacity] duration-300 ease-(--ease-out) motion-reduce:transition-none\",\n          shown && \"pointer-events-none min-h-60 opacity-70 blur-[5px] select-none\",\n          phase === \"open\" && shown && \"opacity-100 blur-none\",\n        )}\n      >\n        {children}\n      </div>\n      <AnimatePresence>\n        {shown && (\n          <motion.div\n            role=\"region\"\n            aria-labelledby={titleId}\n            className=\"absolute inset-0 z-10 flex flex-col items-center justify-center gap-1.5 bg-[radial-gradient(ellipse_at_center,var(--card)_30%,color-mix(in_oklab,var(--card)_72%,transparent)_62%,transparent)] p-6 text-center\"\n            initial={reduce ? false : { opacity: 0 }}\n            animate={{ opacity: 1 }}\n            exit={reduce ? { opacity: 0, transition: INSTANT } : { opacity: 0, transition: { duration: 0.2, ease: EASE_OUT } }}\n            transition={reduce ? INSTANT : FADE_IN}\n          >\n            <Heading {...parts} center />\n            {description && <p className=\"max-w-xs text-[13px] text-muted-foreground\">{description}</p>}\n            <Actions {...parts} center />\n          </motion.div>\n        )}\n      </AnimatePresence>\n    </div>\n  )\n}\n\nexport { FeatureGate, type FeatureGateProps, type FeatureGateVariant, type FeatureGateColor }\n",
      "type": "registry:component",
      "target": "@components/ikas/feature-gate.tsx"
    }
  ],
  "docs": "https://builders.ikas.com/tr/docs/app-development/ui-kit/components/feature-gate",
  "categories": [
    "feedback"
  ],
  "type": "registry:component"
}