When using characters within the pattern that are being used as delimiters, typically they must be escaped (there are oddball circumstances that don't require this, but its not likely you'll run into such situations, so we won't go there).
Also note:
Quantifiers like {1} is pointless.. so (f|ht){1} is the exact same as simply (f|ht)
Since you are using the i modifier after the closing delimiter (which is case insensitive), you don't need to specifiy both a-z and A-Z in your pattern. The i modifier already takes care of this.
There are a few unecessary nested capturing groups. Since the goal is to simply replace the whole pattern, we can eliminate the groups that serve nothing useful and make the alternation groups non capturing (via using ?: at the start within the parenthesis).
So your current pattern could become something like:
'~(?:f|ht)tp://[-a-z0-9@:%_+.\~#?&/=]+~i' // use \\0 instead of \\1 in your replacement argument
If you use the following prior to your preg_replace line:
setlocale(LC_CTYPE, 'C');
this enables you to make your pattern even more condensed. The character class short hand \w (word character) is equal to a-zA-Z0-9_ (without that small snippet just mentioned, \w might include even more characters, depending on your locale) so your pattern could be simplified somewhat:
'~(?:f|ht)tp://[-\w@:%+.\~#?&/=]+~i'
Granted, despite all of this, any patterns in this thread will a) require the link to start with ftp or http, and can support inappropriate links like [url]http://&&&&???###..........=====[/url] , but depending on the situation, this may not be an issue.